View Javadoc

1   package org.codehaus.mojo.javascript;
2   
3   /*
4    * Derivative Work
5    * Copyright 2010 SOFTEC sa. All rights reserved.
6    *
7    * Original Work
8    * Copyright 2001-2005 The Apache Software Foundation.
9    *
10   * Licensed under the Apache License, Version 2.0 (the "License");
11   * you may not use this file except in compliance with the License.
12   * You may obtain a copy of the License at
13   *
14   *      http://www.apache.org/licenses/LICENSE-2.0
15   *
16   * Unless required by applicable law or agreed to in writing, software
17   * distributed under the License is distributed on an "AS IS" BASIS,
18   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19   * See the License for the specific language governing permissions and
20   * limitations under the License.
21   */
22  
23  import org.apache.commons.lang.StringUtils;
24  import org.apache.maven.artifact.Artifact;
25  import org.apache.maven.artifact.factory.ArtifactFactory;
26  import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
27  import org.apache.maven.artifact.repository.ArtifactRepository;
28  import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
29  import org.apache.maven.artifact.resolver.ArtifactResolver;
30  import org.apache.maven.artifact.versioning.VersionRange;
31  import org.apache.maven.plugin.AbstractMojo;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.project.MavenProject;
34  import org.codehaus.mojo.javascript.compress.CompressionException;
35  import org.codehaus.mojo.javascript.compress.IsolatedClassLoader;
36  import org.codehaus.mojo.javascript.compress.JSCompressor;
37  import org.codehaus.mojo.javascript.compress.JSMinCompressor;
38  import org.codehaus.plexus.util.DirectoryScanner;
39  import org.codehaus.plexus.util.FileUtils;
40  import org.codehaus.plexus.util.IOUtil;
41  
42  import java.io.*;
43  import java.text.NumberFormat;
44  import java.util.Collections;
45  import java.util.HashMap;
46  import java.util.List;
47  import java.util.Map;
48  
49  /**
50   * Abstact mojo for compressing JavaScripts.
51   * 
52   * @author <a href="mailto:nicolas@apache.org">nicolas De Loof</a>
53   */
54  public abstract class AbstractCompressMojo
55      extends AbstractMojo
56  {
57  
58      /**
59       * Resolves the artifacts and dependencies.
60       * 
61       * @component
62       */
63      private ArtifactResolver artifactResolver;
64  
65      /**
66       * Create Artifact references
67       * 
68       * @component
69       */
70      private ArtifactFactory artifactFactory;
71  
72      /**
73       * The local repository
74       * 
75       * @parameter expression="${localRepository}"
76       * @required
77       */
78      private ArtifactRepository localRepository;
79  
80      /**
81       * For retrieval of artifact's metadata.
82       * 
83       * @component
84       */
85      private ArtifactMetadataSource metadataSource;
86  
87      /**
88       * The remote repositories declared in the pom.
89       * 
90       * @parameter expression="${project.pluginArtifactRepositories}"
91       */
92      private List remoteRepositories;
93  
94      /**
95       *
96       */
97      private static final NumberFormat INTEGER = NumberFormat.getIntegerInstance();
98  
99      private static final String HR = StringUtils.rightPad( "", 78, "-" );
100 
101     /**
102      * The maven project we are working on.
103      * 
104      * @parameter default-value="${project}"
105      * @required
106      * @readonly
107      */
108     MavenProject project;
109 
110     /**
111      * The available compressors
112      */
113     private Map compressors = new HashMap();
114     {
115         compressors.put( "jsmin", new JSMinCompressor() );
116     }
117 
118     /**
119      * Optimization level, from 0 to 9
120      * 
121      * @parameter default-value="9"
122      */
123     private int optimizationLevel;
124 
125     /**
126      * JS Language version (130 for JS 1.3)
127      * 
128      * @parameter default-value="130"
129      */
130     private int languageVersion;
131 
132     /**
133      * The compressor to used. Either "shrinksafe", "yahooui" or "jsmin" for default compressor, 
134 	 * or a custom one provided as an artifact in repo org.codehaus.mojo.javascript:[xxx]-compressor.
135      * 
136      * @parameter default-value="jsmin"
137      */
138     private String compressor;
139 
140     /**
141      * Don't display compression stats
142      * 
143      * @parameter
144      */
145     private boolean skipStats;
146 
147     private static final String[] DEFAULT_INCLUDES = new String[] { "**/*.js" };
148 
149     /**
150      * Inclusion patterns
151      * 
152      * @parameter
153      */
154     private String[] includes;
155 
156     /**
157      * Exclusion patterns
158      * 
159      * @parameter
160      */
161     private String[] excludes;
162 
163     /**
164      * A list of special token to recognize lines to be removed from scripts (debugging
165      * code).
166      * 
167      * @parameter
168      */
169     private String[] strips;
170 
171     /**
172      * A special token to recognize lines to be removed from scripts (debugging
173      * code).
174      *
175      * @parameter
176      */
177     private String strip;
178 
179     /**
180      * {@inheritDoc}
181      * 
182      * @see org.apache.maven.plugin.AbstractMojo#execute()
183      */
184     public void execute()
185         throws MojoExecutionException
186     {
187         DirectoryScanner scanner = getDirectoryScanner();
188         scanner.scan();
189         String[] files = scanner.getIncludedFiles();
190 
191         // if ( !Context.isValidOptimizationLevel( optimizationLevel ) )
192         // {
193         // throw new MojoExecutionException( "optimizationLevel is invalid" );
194         // }
195         // if ( !Context.isValidLanguageVersion( languageVersion ) )
196         // {
197         // throw new MojoExecutionException( "languageVersion is invalid" );
198         // }
199 
200         JSCompressor jscompressor = getCompressor();
201 
202         logStats( HR );
203         getOutputDirectory().mkdirs();
204         long saved = 0;
205         for ( int i = 0; i < files.length; i++ )
206         {
207             String file = files[i];
208             saved = compress( jscompressor, file );
209         }
210         logStats( HR );
211         logStats( "compression saved " + INTEGER.format( saved ) + " bytes" );
212     }
213 
214     protected DirectoryScanner getDirectoryScanner() {
215         DirectoryScanner scanner = new DirectoryScanner();
216         scanner.setBasedir( getSourceDirectory() );
217         if ( includes == null )
218         {
219             includes = DEFAULT_INCLUDES;
220         }
221         scanner.setIncludes( includes );
222         scanner.addDefaultExcludes();
223         if ( excludes != null )
224         {
225             scanner.setExcludes( excludes );
226         }
227         return scanner;
228     }
229 
230     private File stripDebugs( String name, File file )
231         throws MojoExecutionException
232     {
233         if ( strip == null && (strips == null || strips.length == 0))
234         {
235             return file;
236         }
237 
238         File stripped = new File( getStrippedDirectory(), name );
239         stripped.getParentFile().mkdirs();
240         if ( file.equals( stripped ) )
241         {
242             try
243             {
244                 File temp = File.createTempFile( "stripped", ".js" );
245                 stripDebugs(file, temp);
246                 FileUtils.copyFile( temp, file );
247                 temp.delete();
248                 return file;
249             }
250             catch ( IOException e )
251             {
252                 throw new MojoExecutionException( "Error creating temp file for stripping", e );
253             }
254         }
255         else
256         {
257             stripDebugs(file, stripped);
258             return stripped;
259         }
260     }
261 
262     private void stripDebugs( File file, File stripped )
263             throws MojoExecutionException
264     {
265         try
266         {
267             BufferedReader reader = new BufferedReader( new FileReader( file ) );
268             PrintWriter writer = new PrintWriter( stripped );
269             String line;
270             while ( ( line = reader.readLine() ) != null )
271             {
272                 String trimmed = line.trim();
273                 boolean stripLine = (strip != null && trimmed.startsWith( strip ));
274                 if( strips != null ) {
275                     for( int i=0, len = strips.length; !stripLine && i < len; i++ ) {
276                         stripLine = trimmed.startsWith( strips[i] );
277                     }
278                 }
279                 if ( !stripLine )
280                 {
281                     writer.println( line );
282                 }
283             }
284             IOUtil.close( reader );
285             IOUtil.close( writer );
286         }
287         catch ( IOException e )
288         {
289             throw new MojoExecutionException( "Failed to strip debug code in " + file, e );
290         }
291     }
292 
293     private JSCompressor getCompressor()
294         throws MojoExecutionException
295     {
296         if ( compressors.containsKey( compressor ) )
297         {
298             return (JSCompressor) compressors.get( compressor );
299         }
300 
301         // Inspired by the surefire plugin
302         // allows to use multiple compressor that rely on modifier Rhino engines
303         // without dependencies/classpath conflicts
304 
305         String id = compressor.toLowerCase() + "-compressor";
306 
307         // TODO don't have version hardcoded
308         Artifact compressorArtifact =
309             artifactFactory.createDependencyArtifact( "org.codehaus.mojo.javascript", id,
310                 VersionRange.createFromVersion( "1.0-softec" ), "jar", null,
311                 Artifact.SCOPE_RUNTIME );
312 
313         Artifact originatingArtifact =
314             artifactFactory.createBuildArtifact( "dummy", "dummy", "1.0", "jar" );
315 
316         ArtifactResolutionResult dependencies;
317         try
318         {
319             dependencies =
320                 artifactResolver.resolveTransitively( Collections.singleton( compressorArtifact ),
321                     originatingArtifact, localRepository, remoteRepositories, metadataSource, null );
322         }
323         catch ( Exception e )
324         {
325             getLog().info( "Failed to load compressor artifact " + compressorArtifact.toString() );
326             throw new MojoExecutionException( "Failed to load compressor artifact"
327                 + compressorArtifact.toString(), e );
328         }
329 
330         IsolatedClassLoader classLoader = new IsolatedClassLoader( dependencies.getArtifacts(),
331             this.getClass().getClassLoader() );
332 
333         compressor = StringUtils.capitalize( compressor );
334         String compressorClassName =
335             "org.codehaus.mojo.javascript.compress." + compressor + "Compressor";
336         Class compressorClass;
337         try
338         {
339             compressorClass = classLoader.loadClass( compressorClassName );
340         }
341         catch ( ClassNotFoundException e )
342         {
343             getLog().info( "Failed to load compressor class " + compressorClassName );
344             throw new MojoExecutionException( "Failed to load compressor class"
345                 + compressorClassName, e );
346         }
347 
348         JSCompressor jscompressor;
349         try
350         {
351             jscompressor = (JSCompressor) compressorClass.newInstance();
352             jscompressor.setLogger(new MojoJSCompressorLogger(getLog()));
353         }
354         catch ( Exception e )
355         {
356             getLog().info(
357                 "Failed to create a isolated-classloader proxy for " + compressorClassName );
358             throw new MojoExecutionException( "Failed to create a isolated-classloader proxy for "
359                 + compressorClassName, e );
360         }
361         getLog().info( "Compressing javascript using " + compressor );
362 
363         compressors.put( compressor, jscompressor );
364         return jscompressor;
365     }
366 
367     private long compress( JSCompressor jscompressor, String file )
368         throws MojoExecutionException
369     {
370         String name = file;
371         if ( getExtension() != null )
372         {
373             int ext = file.lastIndexOf( '.' );
374             name = file.substring( 0, ext ) + "-" + getExtension() + file.substring( ext );
375         }
376         File compressed = new File( getOutputDirectory(), name );
377         compressed.getParentFile().mkdirs();
378         File in = new File( getSourceDirectory(), file );
379         if ( in.equals( compressed ) )
380         {
381             try
382             {
383                 File temp = File.createTempFile( "compress", ".js" );
384                 long size = compress( name, in, temp, jscompressor );
385                 FileUtils.copyFile( temp, compressed );
386                 temp.delete();
387                 return size;
388             }
389             catch ( IOException e )
390             {
391                 throw new MojoExecutionException( "Error creating temp file for compression", e );
392             }
393         }
394         else
395         {
396             return compress( name, in, compressed, jscompressor );
397         }
398     }
399 
400     private long compress( String name, File in, File compressed, JSCompressor jscompressor )
401         throws MojoExecutionException
402     {
403         if ( in.length() > 0 )
404         {
405             File stripped = stripDebugs( name, in );
406             try
407             {
408                 jscompressor.compress( stripped, compressed, optimizationLevel, languageVersion );
409             }
410             catch ( CompressionException e )
411             {
412                 throw new MojoExecutionException( "Failed to compress Javascript file "
413                     + e.getScript(), e );
414             }
415             String describe = in.getName() + " (" + INTEGER.format( in.length() ) + " bytes) ";
416             String title = StringUtils.rightPad( describe, 50, "." );
417             logStats( title + " compressed at " + ratio( compressed, in ) + "%" );
418             return in.length() - compressed.length();
419         }
420         else
421         {
422             try
423             {
424                 compressed.createNewFile();
425                 getLog().info( in.getName() + " was zero length; not compressed." );
426                 return 0;
427             }
428             catch ( IOException e )
429             {
430                 throw new MojoExecutionException( "Error handling zero length file.", e );
431             }
432         }
433     }
434 
435     private long ratio( File compressed, File in )
436     {
437         long length = in.length();
438         if ( length == 0 )
439         {
440             return 0;
441         }
442         return ( ( ( length - compressed.length() ) * 100 ) / length );
443     }
444 
445     private void logStats( String line )
446     {
447         if ( skipStats )
448         {
449             return;
450         }
451         getLog().info( line );
452     }
453 
454     /**
455      * @return the extension to append to compressed scripts.
456      */
457     public abstract String getExtension();
458 
459     /**
460      * @return the outputDirectory
461      */
462     protected abstract File getOutputDirectory();
463 
464     /**
465      * @return the outputDirectory
466      */
467     protected abstract File getStrippedDirectory();
468 
469     /**
470      * @return the sourceDirectory
471      */
472     protected abstract File getSourceDirectory();
473 
474     protected void setLocalRepository( ArtifactRepository localRepository )
475     {
476         this.localRepository = localRepository;
477     }
478 }