1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package net.alchim31.maven.yuicompressor;
21
22 import com.yahoo.platform.yui.compressor.CssCompressor;
23 import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
24
25 import java.io.File;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.io.OutputStreamWriter;
30 import java.nio.charset.Charset;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.util.Collection;
34 import java.util.HashSet;
35 import java.util.Locale;
36 import java.util.Set;
37 import java.util.zip.GZIPOutputStream;
38
39 import org.apache.maven.plugin.MojoExecutionException;
40 import org.apache.maven.plugins.annotations.LifecyclePhase;
41 import org.apache.maven.plugins.annotations.Mojo;
42 import org.apache.maven.plugins.annotations.Parameter;
43 import org.codehaus.plexus.util.FileUtils;
44 import org.codehaus.plexus.util.IOUtil;
45
46
47
48
49 @Mojo(name = "compress", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, requiresProject = true, threadSafe = true)
50 public class YuiCompressorMojo extends MojoSupport {
51
52
53
54
55 @Parameter(defaultValue = "${project.build.sourceEncoding}", property = "file.encoding")
56 private String encoding;
57
58
59
60
61 @Parameter(defaultValue = "-min", property = "maven.yuicompressor.suffix")
62 private String suffix;
63
64
65
66
67 @Parameter(defaultValue = "false", property = "maven.yuicompressor.nosuffix")
68 private boolean nosuffix;
69
70
71
72
73 @Parameter(defaultValue = "-1", property = "maven.yuicompressor.linebreakpos")
74 private int linebreakpos;
75
76
77 @Parameter(defaultValue = "false", property = "maven.yuicompressor.nocompress")
78 private boolean nocompress;
79
80
81
82
83 @Parameter(defaultValue = "false", property = "maven.yuicompressor.nomunge")
84 private boolean nomunge;
85
86
87
88
89 @Parameter(defaultValue = "false", property = "maven.yuicompressor.preserveAllSemiColons")
90 private boolean preserveAllSemiColons;
91
92
93
94
95 @Parameter(defaultValue = "false", property = "maven.yuicompressor.disableOptimizations")
96 private boolean disableOptimizations;
97
98
99
100
101
102 @Parameter(defaultValue = "false", property = "maven.yuicompressor.force")
103 private boolean force;
104
105
106
107
108
109 @Parameter
110 private Aggregation[] aggregations;
111
112
113
114
115 @Parameter(defaultValue = "false", property = "maven.yuicompressor.gzip")
116 private boolean gzip;
117
118
119 @Parameter(defaultValue = "9", property = "maven.yuicompressor.level")
120 private int level;
121
122
123
124
125 @Parameter(defaultValue = "true", property = "maven.yuicompressor.statistics")
126 private boolean statistics;
127
128
129 @Parameter(defaultValue = "false", property = "maven.yuicompressor.preProcessAggregates")
130 private boolean preProcessAggregates;
131
132
133 @Parameter(defaultValue = "true", property = "maven.yuicompressor.useSmallestFile")
134 private boolean useSmallestFile;
135
136
137 private long inSizeTotal;
138
139
140 private long outSizeTotal;
141
142
143 private Set<String> incrementalFiles;
144
145 @Override
146 protected String[] getDefaultIncludes() {
147 return new String[] { "**/*.css", "**/*.js" };
148 }
149
150 @Override
151 public void beforeProcess() throws IOException {
152 if (nosuffix) {
153 suffix = "";
154 }
155
156 if (preProcessAggregates) {
157 aggregate();
158 }
159 }
160
161 @Override
162 protected void afterProcess() throws IOException {
163 if (statistics && inSizeTotal > 0) {
164 getLog().info(String.format("total input (%db) -> output (%db)[%d%%]", inSizeTotal, outSizeTotal,
165 outSizeTotal * 100 / inSizeTotal));
166 }
167
168 if (!preProcessAggregates) {
169 aggregate();
170 }
171 }
172
173
174
175
176
177
178
179 private void aggregate() throws IOException {
180 if (aggregations == null) {
181 return;
182 }
183
184 Set<File> previouslyIncludedFiles = new HashSet<>();
185 for (Aggregation aggregation : aggregations) {
186 getLog().info("generate aggregation : " + aggregation.getOutput());
187 Collection<File> aggregatedFiles = aggregation.run(previouslyIncludedFiles, buildContext, incrementalFiles);
188 previouslyIncludedFiles.addAll(aggregatedFiles);
189
190 File gzipped = gzipIfRequested(aggregation.getOutput());
191 if (statistics) {
192 if (gzipped != null) {
193 getLog().info(String.format("%s (%db) -> %s (%db)[%d%%]", aggregation.getOutput().getName(),
194 aggregation.getOutput().length(), gzipped.getName(), gzipped.length(),
195 ratioOfSize(aggregation.getOutput(), gzipped)));
196 } else if (aggregation.getOutput().exists()) {
197 getLog().info(String.format("%s (%db)", aggregation.getOutput().getName(),
198 aggregation.getOutput().length()));
199 } else {
200 getLog().warn(String.format("%s not created", aggregation.getOutput().getName()));
201 }
202 }
203 }
204 }
205
206 @Override
207 protected void processFile(SourceFile src) throws IOException, MojoExecutionException {
208 File inFile = src.toFile();
209 getLog().debug("on incremental build only compress if input file has Delta");
210 if (buildContext.isIncremental()) {
211 if (!buildContext.hasDelta(inFile)) {
212 if (getLog().isInfoEnabled()) {
213 getLog().info("nothing to do, " + inFile + " has no Delta");
214 }
215 return;
216 }
217 if (incrementalFiles == null) {
218 incrementalFiles = new HashSet<>();
219 }
220 }
221
222 if (getLog().isDebugEnabled()) {
223 getLog().debug("compress file :" + src.toFile() + " to " + src.toDestFile(suffix));
224 }
225
226 File outFile = src.toDestFile(suffix);
227 if (!nosuffix && isMinifiedFile(inFile)) {
228 return;
229 }
230 getLog().debug("only compress if input file is younger than existing output file");
231 if (!force && outFile.exists() && outFile.lastModified() > inFile.lastModified()) {
232 if (getLog().isInfoEnabled()) {
233 getLog().info("nothing to do, " + outFile
234 + " is younger than original, use 'force' option or clean your target");
235 }
236 return;
237 }
238 File outFileTmp = Path.of(outFile.getCanonicalPath() + ".tmp").toFile();
239 FileUtils.forceDelete(outFileTmp);
240
241 if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
242 throw new MojoExecutionException("Cannot create resource output directory: " + outFile.getParentFile());
243 }
244 getLog().debug("use a temporary outputfile (in case in == out)");
245
246 try (InputStreamReader in = new InputStreamReader(Files.newInputStream(inFile.toPath()),
247 Charset.forName(encoding));
248
249 OutputStreamWriter out = new OutputStreamWriter(Files.newOutputStream(outFileTmp.toPath()),
250 Charset.forName(encoding));) {
251
252 getLog().debug("start compression");
253 try {
254 if (nocompress) {
255 getLog().info("No compression is enabled");
256 IOUtil.copy(in, out);
257 } else if (".js".equalsIgnoreCase(src.getExtension())) {
258 JavaScriptCompressor compressor = new JavaScriptCompressor(in, jsErrorReporter);
259 compressor.compress(out, linebreakpos, !nomunge, jswarn, preserveAllSemiColons,
260 disableOptimizations);
261 } else if (".css".equalsIgnoreCase(src.getExtension())) {
262 compressCss(in, out);
263 }
264 } catch (IndexOutOfBoundsException e) {
265
266
267
268 getLog().warn("YUICompressor failed on file: " + inFile.getName()
269 + " due to IndexOutOfBoundsException. Skipping this file.");
270 return;
271 }
272 getLog().debug("end compression");
273 }
274
275 boolean outputIgnored = useSmallestFile && inFile.length() < outFile.length();
276 if (outputIgnored) {
277 FileUtils.forceDelete(outFileTmp);
278 FileUtils.copyFile(inFile, outFile);
279 getLog().debug("output greater than input, using original instead");
280 } else {
281 FileUtils.forceDelete(outFile);
282 FileUtils.rename(outFileTmp, outFile);
283 buildContext.refresh(outFile);
284 }
285
286 if (buildContext.isIncremental()) {
287 incrementalFiles.add(outFile.getCanonicalPath());
288 }
289
290 File gzipped = gzipIfRequested(outFile);
291 if (statistics) {
292 inSizeTotal += inFile.length();
293 outSizeTotal += outFile.length();
294
295 String fileStatistics;
296 if (outputIgnored) {
297 fileStatistics = String.format(
298 "%s (%db) -> %s (%db)[compressed output discarded (exceeded input size)]", inFile.getName(),
299 inFile.length(), outFile.getName(), outFile.length());
300 } else {
301 fileStatistics = String.format("%s (%db) -> %s (%db)[%d%%]", inFile.getName(), inFile.length(),
302 outFile.getName(), outFile.length(), ratioOfSize(inFile, outFile));
303 }
304
305 if (gzipped != null) {
306 fileStatistics = fileStatistics + String.format(" -> %s (%db)[%d%%]", gzipped.getName(),
307 gzipped.length(), ratioOfSize(inFile, gzipped));
308 }
309 getLog().info(fileStatistics);
310 }
311 }
312
313
314
315
316
317
318
319
320
321 private void compressCss(InputStreamReader in, OutputStreamWriter out) throws IOException {
322 try {
323 CssCompressor compressor = new CssCompressor(in);
324 compressor.compress(out, linebreakpos);
325 } catch (IllegalArgumentException e) {
326 throw new IllegalArgumentException(
327 "Unexpected characters found in CSS file. Ensure that the CSS file does not contain '$', and try again",
328 e);
329 }
330 }
331
332
333
334
335
336
337
338
339
340
341
342
343 protected File gzipIfRequested(File file) throws IOException {
344 if (!gzip || file == null || !file.exists() || ".gz".equalsIgnoreCase(FileUtils.getExtension(file.getName()))) {
345 return null;
346 }
347 File gzipped = Path.of(file.getCanonicalFile() + ".gz").toFile();
348 getLog().debug(String.format("create gzip version : %s", gzipped.getName()));
349 try (InputStream in = Files.newInputStream(file.toPath());
350 GZIPOutputStream out = new GZIPOutputStream(buildContext.newFileOutputStream(gzipped)) {
351 {
352 def.setLevel(level);
353 }
354 };) {
355 IOUtil.copy(in, out);
356 }
357 return gzipped;
358 }
359
360
361
362
363
364
365
366
367
368
369
370 protected long ratioOfSize(File file100, File fileX) {
371 long v100 = Math.max(file100.length(), 1);
372 long vX = Math.max(fileX.length(), 1);
373 return vX * 100 / v100;
374 }
375
376
377
378
379
380
381
382
383
384 private boolean isMinifiedFile(File inFile) {
385 String filename = inFile.getName().toLowerCase(Locale.getDefault());
386 return filename.endsWith(suffix + ".js") || filename.endsWith(suffix + ".css");
387 }
388
389 }