YuiCompressorMojo.java
/*
* YuiCompressor Maven plugin
*
* Copyright 2012-2025 Hazendaz.
*
* Licensed under the GNU Lesser General Public License (LGPL),
* version 2.1 or later (the "License").
* You may not use this file except in compliance with the License.
* You may read the licence in the 'lgpl.txt' file in the root folder of
* project or obtain a copy at
*
* https://www.gnu.org/licenses/lgpl-2.1.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.alchim31.maven.yuicompressor;
import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.zip.GZIPOutputStream;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
/**
* Apply compression on JS and CSS (using YUI Compressor).
*/
@Mojo(name = "compress", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, requiresProject = true, threadSafe = true)
public class YuiCompressorMojo extends MojoSupport {
/**
* Read the input file using "encoding".
*/
@Parameter(defaultValue = "${project.build.sourceEncoding}", property = "file.encoding")
private String encoding;
/**
* The output filename suffix.
*/
@Parameter(defaultValue = "-min", property = "maven.yuicompressor.suffix")
private String suffix;
/**
* If no "suffix" must be add to output filename (maven's configuration manage empty suffix like default).
*/
@Parameter(defaultValue = "false", property = "maven.yuicompressor.nosuffix")
private boolean nosuffix;
/**
* Insert line breaks in output after the specified column number.
*/
@Parameter(defaultValue = "-1", property = "maven.yuicompressor.linebreakpos")
private int linebreakpos;
/** [js only] No compression. */
@Parameter(defaultValue = "false", property = "maven.yuicompressor.nocompress")
private boolean nocompress;
/**
* [js only] Minify only, do not obfuscate.
*/
@Parameter(defaultValue = "false", property = "maven.yuicompressor.nomunge")
private boolean nomunge;
/**
* [js only] Preserve unnecessary semicolons.
*/
@Parameter(defaultValue = "false", property = "maven.yuicompressor.preserveAllSemiColons")
private boolean preserveAllSemiColons;
/**
* [js only] disable all micro optimizations.
*/
@Parameter(defaultValue = "false", property = "maven.yuicompressor.disableOptimizations")
private boolean disableOptimizations;
/**
* force the compression of every files, else if compressed file already exists and is younger than source file,
* nothing is done.
*/
@Parameter(defaultValue = "false", property = "maven.yuicompressor.force")
private boolean force;
/**
* a list of aggregation/concatenation to do after processing, for example to create big js files that contain
* several small js files. Aggregation could be done on any type of file (js, css, ..).
*/
@Parameter
private Aggregation[] aggregations;
/**
* request to create a gzipped version of the yuicompressed/aggregation files.
*/
@Parameter(defaultValue = "false", property = "maven.yuicompressor.gzip")
private boolean gzip;
/** gzip level. */
@Parameter(defaultValue = "9", property = "maven.yuicompressor.level")
private int level;
/**
* show statistics (compression ratio).
*/
@Parameter(defaultValue = "true", property = "maven.yuicompressor.statistics")
private boolean statistics;
/** aggregate files before minify. */
@Parameter(defaultValue = "false", property = "maven.yuicompressor.preProcessAggregates")
private boolean preProcessAggregates;
/** use the input file as output when the compressed file is larger than the original. */
@Parameter(defaultValue = "true", property = "maven.yuicompressor.useSmallestFile")
private boolean useSmallestFile;
/** The in size total. */
private long inSizeTotal;
/** The out size total. */
private long outSizeTotal;
/** Keep track of updated files for aggregation on incremental builds. */
private Set<String> incrementalFiles;
@Override
protected String[] getDefaultIncludes() {
return new String[] { "**/*.css", "**/*.js" };
}
@Override
public void beforeProcess() throws IOException {
if (nosuffix) {
suffix = "";
}
if (preProcessAggregates) {
aggregate();
}
}
@Override
protected void afterProcess() throws IOException {
if (statistics && inSizeTotal > 0) {
getLog().info(String.format("total input (%db) -> output (%db)[%d%%]", inSizeTotal, outSizeTotal,
outSizeTotal * 100 / inSizeTotal));
}
if (!preProcessAggregates) {
aggregate();
}
}
/**
* Aggregate.
*
* @throws IOException
* the IO exception
*/
private void aggregate() throws IOException {
if (aggregations != null) {
Set<File> previouslyIncludedFiles = new HashSet<>();
for (Aggregation aggregation : aggregations) {
getLog().info("generate aggregation : " + aggregation.getOutput());
Collection<File> aggregatedFiles = aggregation.run(previouslyIncludedFiles, buildContext,
incrementalFiles);
previouslyIncludedFiles.addAll(aggregatedFiles);
File gzipped = gzipIfRequested(aggregation.getOutput());
if (statistics) {
if (gzipped != null) {
getLog().info(String.format("%s (%db) -> %s (%db)[%d%%]", aggregation.getOutput().getName(),
aggregation.getOutput().length(), gzipped.getName(), gzipped.length(),
ratioOfSize(aggregation.getOutput(), gzipped)));
} else if (aggregation.getOutput().exists()) {
getLog().info(String.format("%s (%db)", aggregation.getOutput().getName(),
aggregation.getOutput().length()));
} else {
getLog().warn(String.format("%s not created", aggregation.getOutput().getName()));
}
}
}
}
}
@Override
protected void processFile(SourceFile src) throws IOException, MojoExecutionException {
File inFile = src.toFile();
getLog().debug("on incremental build only compress if input file has Delta");
if (buildContext.isIncremental()) {
if (!buildContext.hasDelta(inFile)) {
if (getLog().isInfoEnabled()) {
getLog().info("nothing to do, " + inFile + " has no Delta");
}
return;
}
if (incrementalFiles == null) {
incrementalFiles = new HashSet<>();
}
}
if (getLog().isDebugEnabled()) {
getLog().debug("compress file :" + src.toFile() + " to " + src.toDestFile(suffix));
}
File outFile = src.toDestFile(suffix);
if (!nosuffix && isMinifiedFile(inFile)) {
return;
}
getLog().debug("only compress if input file is younger than existing output file");
if (!force && outFile.exists() && outFile.lastModified() > inFile.lastModified()) {
if (getLog().isInfoEnabled()) {
getLog().info("nothing to do, " + outFile
+ " is younger than original, use 'force' option or clean your target");
}
return;
}
File outFileTmp = Path.of(outFile.getCanonicalPath() + ".tmp").toFile();
FileUtils.forceDelete(outFileTmp);
if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
throw new MojoExecutionException("Cannot create resource output directory: " + outFile.getParentFile());
}
getLog().debug("use a temporary outputfile (in case in == out)");
try (InputStreamReader in = new InputStreamReader(Files.newInputStream(inFile.toPath()),
Charset.forName(encoding));
/* outFileTmp will be deleted create with FileOutputStream */
OutputStreamWriter out = new OutputStreamWriter(Files.newOutputStream(outFileTmp.toPath()),
Charset.forName(encoding));) {
getLog().debug("start compression");
if (nocompress) {
getLog().info("No compression is enabled");
IOUtil.copy(in, out);
} else if (".js".equalsIgnoreCase(src.getExtension())) {
JavaScriptCompressor compressor = new JavaScriptCompressor(in, jsErrorReporter);
compressor.compress(out, linebreakpos, !nomunge, jswarn, preserveAllSemiColons, disableOptimizations);
} else if (".css".equalsIgnoreCase(src.getExtension())) {
compressCss(in, out);
}
getLog().debug("end compression");
}
boolean outputIgnored = useSmallestFile && inFile.length() < outFile.length();
if (outputIgnored) {
FileUtils.forceDelete(outFileTmp);
FileUtils.copyFile(inFile, outFile);
getLog().debug("output greater than input, using original instead");
} else {
FileUtils.forceDelete(outFile);
FileUtils.rename(outFileTmp, outFile);
buildContext.refresh(outFile);
}
if (buildContext.isIncremental()) {
incrementalFiles.add(outFile.getCanonicalPath());
}
File gzipped = gzipIfRequested(outFile);
if (statistics) {
inSizeTotal += inFile.length();
outSizeTotal += outFile.length();
String fileStatistics;
if (outputIgnored) {
fileStatistics = String.format(
"%s (%db) -> %s (%db)[compressed output discarded (exceeded input size)]", inFile.getName(),
inFile.length(), outFile.getName(), outFile.length());
} else {
fileStatistics = String.format("%s (%db) -> %s (%db)[%d%%]", inFile.getName(), inFile.length(),
outFile.getName(), outFile.length(), ratioOfSize(inFile, outFile));
}
if (gzipped != null) {
fileStatistics = fileStatistics + String.format(" -> %s (%db)[%d%%]", gzipped.getName(),
gzipped.length(), ratioOfSize(inFile, gzipped));
}
getLog().info(fileStatistics);
}
}
/**
* Compress css.
*
* @param in
* the in
* @param out
* the out
*/
private void compressCss(InputStreamReader in, OutputStreamWriter out) throws IOException {
try {
CssCompressor compressor = new CssCompressor(in);
compressor.compress(out, linebreakpos);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Unexpected characters found in CSS file. Ensure that the CSS file does not contain '$', and try again",
e);
}
}
/**
* Gzip if requested.
*
* @param file
* the file
*
* @return the file
*
* @throws IOException
* the IO exception
*/
protected File gzipIfRequested(File file) throws IOException {
if (!gzip || file == null || !file.exists() || ".gz".equalsIgnoreCase(FileUtils.getExtension(file.getName()))) {
return null;
}
File gzipped = Path.of(file.getCanonicalFile() + ".gz").toFile();
getLog().debug(String.format("create gzip version : %s", gzipped.getName()));
try (InputStream in = Files.newInputStream(file.toPath());
GZIPOutputStream out = new GZIPOutputStream(buildContext.newFileOutputStream(gzipped)) {
{
def.setLevel(level);
}
};) {
IOUtil.copy(in, out);
}
return gzipped;
}
/**
* Ratio of size.
*
* @param file100
* the file 100
* @param fileX
* the file X
*
* @return the long
*/
protected long ratioOfSize(File file100, File fileX) {
long v100 = Math.max(file100.length(), 1);
long vX = Math.max(fileX.length(), 1);
return vX * 100 / v100;
}
/**
* Checks if is minified file.
*
* @param inFile
* the in file
*
* @return true, if is minified file
*/
private boolean isMinifiedFile(File inFile) {
String filename = inFile.getName().toLowerCase(Locale.getDefault());
return filename.endsWith(suffix + ".js") || filename.endsWith(suffix + ".css");
}
}