View Javadoc
1   package net_alchim31_maven_yuicompressor;
2   
3   import java.io.File;
4   import java.io.FileInputStream;
5   import java.io.IOException;
6   import java.io.InputStreamReader;
7   import java.io.OutputStreamWriter;
8   import java.util.Collection;
9   import java.util.HashSet;
10  import java.util.Set;
11  import java.util.zip.GZIPOutputStream;
12  
13  import org.apache.maven.plugin.MojoExecutionException;
14  import org.codehaus.plexus.util.FileUtils;
15  import org.codehaus.plexus.util.IOUtil;
16  
17  import com.yahoo.platform.yui.compressor.CssCompressor;
18  import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
19  
20  /**
21   * Apply compression on JS and CSS (using YUI Compressor).
22   *
23   * @goal compress
24   * @phase process-resources
25   *
26   * @author David Bernard
27   * @created 2007-08-28
28   * @threadSafe
29   */
30  // @SuppressWarnings("unchecked")
31  public class YuiCompressorMojo extends MojoSupport {
32  
33      /**
34       * Read the input file using "encoding".
35       *
36       * @parameter property="file.encoding" default-value="UTF-8"
37       */
38      private String encoding;
39  
40      /**
41       * The output filename suffix.
42       *
43       * @parameter property="maven.yuicompressor.suffix" default-value="-min"
44       */
45      private String suffix;
46  
47      /**
48       * If no "suffix" must be add to output filename (maven's configuration manage empty suffix like default).
49       *
50       * @parameter property="maven.yuicompressor.nosuffix" default-value="false"
51       */
52      private boolean nosuffix;
53  
54      /**
55       * Insert line breaks in output after the specified column number.
56       *
57       * @parameter property="maven.yuicompressor.linebreakpos" default-value="-1"
58       */
59      private int linebreakpos;
60  
61      /**
62       * [js only] No compression
63       *
64       * @parameter property="maven.yuicompressor.nocompress" default-value="false"
65       */
66      private boolean nocompress;
67  
68      /**
69       * [js only] Minify only, do not obfuscate.
70       *
71       * @parameter property="maven.yuicompressor.nomunge" default-value="false"
72       */
73      private boolean nomunge;
74  
75      /**
76       * [js only] Preserve unnecessary semicolons.
77       *
78       * @parameter property="maven.yuicompressor.preserveAllSemiColons" default-value="false"
79       */
80      private boolean preserveAllSemiColons;
81  
82      /**
83       * [js only] disable all micro optimizations.
84       *
85       * @parameter property="maven.yuicompressor.disableOptimizations" default-value="false"
86       */
87      private boolean disableOptimizations;
88  
89      /**
90       * force the compression of every files,
91       * else if compressed file already exists and is younger than source file, nothing is done.
92       *
93       * @parameter property="maven.yuicompressor.force" default-value="false"
94       */
95      private boolean force;
96  
97      /**
98       * a list of aggregation/concatenation to do after processing,
99       * for example to create big js files that contain several small js files.
100      * Aggregation could be done on any type of file (js, css, ...).
101      *
102      * @parameter
103      */
104     private Aggregation[] aggregations;
105 
106     /**
107      * request to create a gzipped version of the yuicompressed/aggregation files.
108      *
109      * @parameter property="maven.yuicompressor.gzip" default-value="false"
110      */
111     private boolean gzip;
112 
113     /**
114      * show statistics (compression ratio).
115      *
116      * @parameter property="maven.yuicompressor.statistics" default-value="true"
117      */
118     private boolean statistics;
119 
120     /**
121      * aggregate files before minify
122      * @parameter property="maven.yuicompressor.preProcessAggregates" default-value="false"
123      */
124     private boolean preProcessAggregates;
125 
126     /**
127      * use the input file as output when the compressed file is larger than the original
128      * @parameter property="maven.yuicompressor.useSmallestFile" default-value="true"
129      */
130     private boolean useSmallestFile;
131 
132     private long inSizeTotal_;
133     private long outSizeTotal_;
134 
135     @Override
136     protected String[] getDefaultIncludes() throws Exception {
137         return new String[]{"**/*.css", "**/*.js"};
138     }
139 
140     @Override
141     public void beforeProcess() throws Exception {
142         if (nosuffix) {
143             suffix = "";
144         }
145 
146         if(preProcessAggregates) aggregate();
147     }
148 
149     @Override
150     protected void afterProcess() throws Exception {
151         if (statistics && (inSizeTotal_ > 0)) {
152             getLog().info(String.format("total input (%db) -> output (%db)[%d%%]", inSizeTotal_, outSizeTotal_, ((outSizeTotal_ * 100)/inSizeTotal_)));
153         }
154 
155         if(!preProcessAggregates) aggregate();
156     }
157 
158     private void aggregate() throws Exception {
159         if (aggregations != null) {
160             Set<File> previouslyIncludedFiles = new HashSet<File>();
161             for(Aggregation aggregation : aggregations) {
162                 getLog().info("generate aggregation : " + aggregation.output);
163                 Collection<File> aggregatedFiles = aggregation.run(previouslyIncludedFiles,buildContext);
164                 previouslyIncludedFiles.addAll(aggregatedFiles);
165 
166                 File gzipped = gzipIfRequested(aggregation.output);
167                 if (statistics) {
168                     if (gzipped != null) {
169                         getLog().info(String.format("%s (%db) -> %s (%db)[%d%%]", aggregation.output.getName(), aggregation.output.length(), gzipped.getName(), gzipped.length(), ratioOfSize(aggregation.output, gzipped)));
170                     } else if (aggregation.output.exists()){
171                         getLog().info(String.format("%s (%db)", aggregation.output.getName(), aggregation.output.length()));
172                     } else {
173                         getLog().warn(String.format("%s not created", aggregation.output.getName()));
174                     }
175                 }
176             }
177         }
178     }
179 
180     @Override
181     protected void processFile(SourceFile src) throws Exception {
182         if (getLog().isDebugEnabled()) {
183             getLog().debug("compress file :" + src.toFile()+ " to " + src.toDestFile(suffix));
184         }
185         File inFile = src.toFile();
186         File outFile = src.toDestFile(suffix);
187 
188         getLog().debug("only compress if input file is younger than existing output file");
189         if (!force && outFile.exists() && (outFile.lastModified() > inFile.lastModified())) {
190             if (getLog().isInfoEnabled()) {
191                 getLog().info("nothing to do, " + outFile + " is younger than original, use 'force' option or clean your target");
192             }
193             return;
194         }
195 
196         InputStreamReader in = null;
197         OutputStreamWriter out = null;
198         File outFileTmp = new File(outFile.getAbsolutePath() + ".tmp");
199         FileUtils.forceDelete(outFileTmp);
200         try {
201             in = new InputStreamReader(new FileInputStream(inFile), encoding);
202             if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
203                 throw new MojoExecutionException( "Cannot create resource output directory: " + outFile.getParentFile() );
204             }
205             getLog().debug("use a temporary outputfile (in case in == out)");
206 
207             getLog().debug("start compression");
208             out = new OutputStreamWriter(buildContext.newFileOutputStream(outFileTmp), encoding);
209             if (nocompress) {
210                 getLog().info("No compression is enabled");
211                 IOUtil.copy(in, out);
212             } else if (".js".equalsIgnoreCase(src.getExtension())) {
213                 JavaScriptCompressor compressor = new JavaScriptCompressor(in, jsErrorReporter_);
214                 compressor.compress(out, linebreakpos, !nomunge, jswarn, preserveAllSemiColons, disableOptimizations);
215             } else if (".css".equalsIgnoreCase(src.getExtension())) {
216                 compressCss(in, out);
217             }
218             getLog().debug("end compression");
219         } finally {
220             IOUtil.close(in);
221             IOUtil.close(out);
222         }
223 
224         boolean outputIgnored = useSmallestFile && inFile.length() < outFile.length();
225         if (outputIgnored) {
226             FileUtils.forceDelete(outFileTmp);
227             FileUtils.copyFile(inFile, outFile);
228             getLog().debug("output greater than input, using original instead");
229         } else {
230             FileUtils.forceDelete(outFile);
231             FileUtils.rename(outFileTmp, outFile);
232             buildContext.refresh(outFile);
233             buildContext.refresh(outFileTmp);
234         }
235 
236         File gzipped = gzipIfRequested(outFile);
237         if (statistics) {
238             inSizeTotal_ += inFile.length();
239             outSizeTotal_ += outFile.length();
240 
241             String fileStatistics;
242             if (outputIgnored) {
243                 fileStatistics = String.format("%s (%db) -> %s (%db)[compressed output discarded (exceeded input size)]", inFile.getName(), inFile.length(), outFile.getName(), outFile.length());
244             } else {
245                 fileStatistics = String.format("%s (%db) -> %s (%db)[%d%%]", inFile.getName(), inFile.length(), outFile.getName(), outFile.length(), ratioOfSize(inFile, outFile));
246             }
247 
248             if (gzipped != null) {
249                 fileStatistics = fileStatistics + String.format(" -> %s (%db)[%d%%]", gzipped.getName(), gzipped.length(), ratioOfSize(inFile, gzipped));
250             }
251             getLog().info(fileStatistics);
252         }
253     }
254 
255     private void compressCss(InputStreamReader in, OutputStreamWriter out)
256             throws IOException {
257         try{
258             CssCompressor compressor = new CssCompressor(in);
259             compressor.compress(out, linebreakpos);
260         }catch(IllegalArgumentException e){
261             throw new IllegalArgumentException(
262                     "Unexpected characters found in CSS file. Ensure that the CSS file does not contain '$', and try again",e);
263         }
264     }
265 
266     protected File gzipIfRequested(File file) throws Exception {
267         if (!gzip || (file == null) || (!file.exists())) {
268             return null;
269         }
270         if (".gz".equalsIgnoreCase(FileUtils.getExtension(file.getName()))) {
271             return null;
272         }
273         File gzipped = new File(file.getAbsolutePath()+".gz");
274         getLog().debug(String.format("create gzip version : %s", gzipped.getName()));
275         GZIPOutputStream out = null;
276         FileInputStream in = null;
277         try {
278             out = new GZIPOutputStream(buildContext.newFileOutputStream(gzipped));
279             in = new FileInputStream(file);
280             IOUtil.copy(in, out);
281         } finally {
282             IOUtil.close(in);
283             IOUtil.close(out);
284         }
285         return gzipped;
286     }
287 
288     protected long ratioOfSize(File file100, File fileX) throws Exception {
289         long v100 = Math.max(file100.length(), 1);
290         long vX = Math.max(fileX.length(), 1);
291         return (vX * 100)/v100;
292     }
293 }