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