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 Set<File> previouslyIncludedFiles = new HashSet<>();
182 for (Aggregation aggregation : aggregations) {
183 getLog().info("generate aggregation : " + aggregation.getOutput());
184 Collection<File> aggregatedFiles = aggregation.run(previouslyIncludedFiles, buildContext,
185 incrementalFiles);
186 previouslyIncludedFiles.addAll(aggregatedFiles);
187
188 File gzipped = gzipIfRequested(aggregation.getOutput());
189 if (statistics) {
190 if (gzipped != null) {
191 getLog().info(String.format("%s (%db) -> %s (%db)[%d%%]", aggregation.getOutput().getName(),
192 aggregation.getOutput().length(), gzipped.getName(), gzipped.length(),
193 ratioOfSize(aggregation.getOutput(), gzipped)));
194 } else if (aggregation.getOutput().exists()) {
195 getLog().info(String.format("%s (%db)", aggregation.getOutput().getName(),
196 aggregation.getOutput().length()));
197 } else {
198 getLog().warn(String.format("%s not created", aggregation.getOutput().getName()));
199 }
200 }
201 }
202 }
203 }
204
205 @Override
206 protected void processFile(SourceFile src) throws IOException, MojoExecutionException {
207 File inFile = src.toFile();
208 getLog().debug("on incremental build only compress if input file has Delta");
209 if (buildContext.isIncremental()) {
210 if (!buildContext.hasDelta(inFile)) {
211 if (getLog().isInfoEnabled()) {
212 getLog().info("nothing to do, " + inFile + " has no Delta");
213 }
214 return;
215 }
216 if (incrementalFiles == null) {
217 incrementalFiles = new HashSet<>();
218 }
219 }
220
221 if (getLog().isDebugEnabled()) {
222 getLog().debug("compress file :" + src.toFile() + " to " + src.toDestFile(suffix));
223 }
224
225 File outFile = src.toDestFile(suffix);
226 if (!nosuffix && isMinifiedFile(inFile)) {
227 return;
228 }
229 getLog().debug("only compress if input file is younger than existing output file");
230 if (!force && outFile.exists() && outFile.lastModified() > inFile.lastModified()) {
231 if (getLog().isInfoEnabled()) {
232 getLog().info("nothing to do, " + outFile
233 + " is younger than original, use 'force' option or clean your target");
234 }
235 return;
236 }
237 File outFileTmp = Path.of(outFile.getCanonicalPath() + ".tmp").toFile();
238 FileUtils.forceDelete(outFileTmp);
239
240 if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
241 throw new MojoExecutionException("Cannot create resource output directory: " + outFile.getParentFile());
242 }
243 getLog().debug("use a temporary outputfile (in case in == out)");
244
245 try (InputStreamReader in = new InputStreamReader(Files.newInputStream(inFile.toPath()),
246 Charset.forName(encoding));
247
248 OutputStreamWriter out = new OutputStreamWriter(Files.newOutputStream(outFileTmp.toPath()),
249 Charset.forName(encoding));) {
250
251 getLog().debug("start compression");
252 if (nocompress) {
253 getLog().info("No compression is enabled");
254 IOUtil.copy(in, out);
255 } else if (".js".equalsIgnoreCase(src.getExtension())) {
256 JavaScriptCompressor compressor = new JavaScriptCompressor(in, jsErrorReporter);
257 compressor.compress(out, linebreakpos, !nomunge, jswarn, preserveAllSemiColons, disableOptimizations);
258 } else if (".css".equalsIgnoreCase(src.getExtension())) {
259 compressCss(in, out);
260 }
261 getLog().debug("end compression");
262 }
263
264 boolean outputIgnored = useSmallestFile && inFile.length() < outFile.length();
265 if (outputIgnored) {
266 FileUtils.forceDelete(outFileTmp);
267 FileUtils.copyFile(inFile, outFile);
268 getLog().debug("output greater than input, using original instead");
269 } else {
270 FileUtils.forceDelete(outFile);
271 FileUtils.rename(outFileTmp, outFile);
272 buildContext.refresh(outFile);
273 }
274
275 if (buildContext.isIncremental()) {
276 incrementalFiles.add(outFile.getCanonicalPath());
277 }
278
279 File gzipped = gzipIfRequested(outFile);
280 if (statistics) {
281 inSizeTotal += inFile.length();
282 outSizeTotal += outFile.length();
283
284 String fileStatistics;
285 if (outputIgnored) {
286 fileStatistics = String.format(
287 "%s (%db) -> %s (%db)[compressed output discarded (exceeded input size)]", inFile.getName(),
288 inFile.length(), outFile.getName(), outFile.length());
289 } else {
290 fileStatistics = String.format("%s (%db) -> %s (%db)[%d%%]", inFile.getName(), inFile.length(),
291 outFile.getName(), outFile.length(), ratioOfSize(inFile, outFile));
292 }
293
294 if (gzipped != null) {
295 fileStatistics = fileStatistics + String.format(" -> %s (%db)[%d%%]", gzipped.getName(),
296 gzipped.length(), ratioOfSize(inFile, gzipped));
297 }
298 getLog().info(fileStatistics);
299 }
300 }
301
302
303
304
305
306
307
308
309
310 private void compressCss(InputStreamReader in, OutputStreamWriter out) throws IOException {
311 try {
312 CssCompressor compressor = new CssCompressor(in);
313 compressor.compress(out, linebreakpos);
314 } catch (IllegalArgumentException e) {
315 throw new IllegalArgumentException(
316 "Unexpected characters found in CSS file. Ensure that the CSS file does not contain '$', and try again",
317 e);
318 }
319 }
320
321
322
323
324
325
326
327
328
329
330
331
332 protected File gzipIfRequested(File file) throws IOException {
333 if (!gzip || file == null || !file.exists() || ".gz".equalsIgnoreCase(FileUtils.getExtension(file.getName()))) {
334 return null;
335 }
336 File gzipped = Path.of(file.getCanonicalFile() + ".gz").toFile();
337 getLog().debug(String.format("create gzip version : %s", gzipped.getName()));
338 try (InputStream in = Files.newInputStream(file.toPath());
339 GZIPOutputStream out = new GZIPOutputStream(buildContext.newFileOutputStream(gzipped)) {
340 {
341 def.setLevel(level);
342 }
343 };) {
344 IOUtil.copy(in, out);
345 }
346 return gzipped;
347 }
348
349
350
351
352
353
354
355
356
357
358
359 protected long ratioOfSize(File file100, File fileX) {
360 long v100 = Math.max(file100.length(), 1);
361 long vX = Math.max(fileX.length(), 1);
362 return vX * 100 / v100;
363 }
364
365
366
367
368
369
370
371
372
373 private boolean isMinifiedFile(File inFile) {
374 String filename = inFile.getName().toLowerCase(Locale.getDefault());
375 return filename.endsWith(suffix + ".js") || filename.endsWith(suffix + ".css");
376 }
377
378 }