/*
* Minify Maven Plugin
* https://github.com/samaxes/minify-maven-plugin
*
* Copyright (c) 2009 samaxes.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 com.samaxes.maven.minify.plugin;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.zip.GZIPOutputStream;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import com.samaxes.maven.minify.common.SourceFilesEnumeration;
import com.samaxes.maven.minify.common.YuiConfig;
import com.samaxes.maven.minify.plugin.MinifyMojo.Engine;
/**
* Abstract class for merging and compressing a files list.
*/
public abstract class ProcessFilesTask implements Callable<Object> {
public static final String TEMP_SUFFIX = ".tmp";
protected final Log log;
protected final boolean verbose;
protected final Integer bufferSize;
protected final String charset;
protected final String suffix;
protected final boolean nosuffix;
protected final boolean skipMerge;
protected final boolean skipMinify;
protected final Engine engine;
protected final YuiConfig yuiConfig;
private final File sourceDir;
private final File targetDir;
private final String mergedFilename;
private final List<File> files = new ArrayList<File>();
private final boolean sourceFilesEmpty;
private final boolean sourceIncludesEmpty;
/**
* Task constructor.
*
* @param log Maven plugin log
* @param verbose display additional info
* @param bufferSize size of the buffer used to read source files
* @param charset if a character set is specified, a byte-to-char variant allows the encoding to be selected.
* Otherwise, only byte-to-byte operations are used
* @param suffix final file name suffix
* @param nosuffix whether to use a suffix for the minified file name or not
* @param skipMerge whether to skip the merge step or not
* @param skipMinify whether to skip the minify step or not
* @param webappSourceDir web resources source directory
* @param webappTargetDir web resources target directory
* @param inputDir directory containing source files
* @param sourceFiles list of source files to include
* @param sourceIncludes list of source files to include
* @param sourceExcludes list of source files to exclude
* @param outputDir directory to write the final file
* @param outputFilename the output file name
* @param engine minify processor engine selected
* @param yuiConfig YUI Compressor configuration
* @throws FileNotFoundException when the given source file does not exist
*/
public ProcessFilesTask(Log log, boolean verbose, Integer bufferSize, String charset, String suffix,
boolean nosuffix, boolean skipMerge, boolean skipMinify, String webappSourceDir, String webappTargetDir,
String inputDir, List<String> sourceFiles, List<String> sourceIncludes, List<String> sourceExcludes,
String outputDir, String outputFilename, Engine engine, YuiConfig yuiConfig) throws FileNotFoundException {
this.log = log;
this.verbose = verbose;
this.bufferSize = bufferSize;
this.charset = charset;
this.suffix = suffix + ".";
this.nosuffix = nosuffix;
this.skipMerge = skipMerge;
this.skipMinify = skipMinify;
this.engine = engine;
this.yuiConfig = yuiConfig;
this.sourceDir = new File(webappSourceDir + File.separator + inputDir);
this.targetDir = new File(webappTargetDir + File.separator + outputDir);
this.mergedFilename = outputFilename;
for (String sourceFilename : sourceFiles) {
addNewSourceFile(mergedFilename, sourceFilename);
}
for (File sourceInclude : getFilesToInclude(sourceIncludes, sourceExcludes)) {
if (!files.contains(sourceInclude)) {
addNewSourceFile(mergedFilename, sourceInclude);
}
}
this.sourceFilesEmpty = sourceFiles.isEmpty();
this.sourceIncludesEmpty = sourceIncludes.isEmpty();
}
/**
* Method executed by the thread.
*
* @throws IOException when the merge or minify steps fail
*/
@Override
public Object call() throws IOException {
synchronized (log) {
String fileType = (this instanceof ProcessCSSFilesTask) ? "CSS" : "JavaScript";
log.info("Starting " + fileType + " task:");
if (!files.isEmpty() && (targetDir.exists() || targetDir.mkdirs())) {
if (skipMerge) {
log.info("Skipping the merge step...");
String sourceBasePath = sourceDir.getAbsolutePath();
for (File mergedFile : files) {
// Create folders to preserve sub-directory structure when only minifying
String originalPath = mergedFile.getAbsolutePath();
String subPath = originalPath.substring(sourceBasePath.length(),
originalPath.lastIndexOf(File.separator));
File targetPath = new File(targetDir.getAbsolutePath() + subPath);
targetPath.mkdirs();
File minifiedFile = new File(targetPath, (nosuffix) ? mergedFile.getName()
: FileUtils.basename(mergedFile.getName()) + suffix
+ FileUtils.getExtension(mergedFile.getName()));
minify(mergedFile, minifiedFile);
}
} else if (skipMinify) {
File mergedFile = new File(targetDir, mergedFilename);
merge(mergedFile);
log.info("Skipping the minify step...");
} else {
File mergedFile = new File(targetDir, (nosuffix) ? mergedFilename + TEMP_SUFFIX : mergedFilename);
merge(mergedFile);
File minifiedFile = new File(targetDir, (nosuffix) ? mergedFilename
: FileUtils.basename(mergedFilename) + suffix + FileUtils.getExtension(mergedFilename));
minify(mergedFile, minifiedFile);
if (nosuffix) {
if (!mergedFile.delete()) {
mergedFile.deleteOnExit();
}
}
}
log.info("");
} else if (!sourceFilesEmpty || !sourceIncludesEmpty) {
// 'files' list will be empty if source file paths or names added to the project's POM are invalid.
log.error("No valid " + fileType + " source files found to process.");
}
}
return null;
}
/**
* Merges a list of source files.
*
* @param mergedFile output file resulting from the merged step
* @throws IOException when the merge step fails
*/
protected void merge(File mergedFile) throws IOException {
try (InputStream sequence = new SequenceInputStream(new SourceFilesEnumeration(log, files, verbose));
OutputStream out = new FileOutputStream(mergedFile);
InputStreamReader sequenceReader = new InputStreamReader(sequence, charset);
OutputStreamWriter outWriter = new OutputStreamWriter(out, charset)) {
log.info("Creating the merged file [" + ((verbose) ? mergedFile.getPath() : mergedFile.getName()) + "].");
IOUtil.copy(sequenceReader, outWriter, bufferSize);
} catch (IOException e) {
log.error("Failed to concatenate files.", e);
throw e;
}
}
/**
* Minifies a source file.
*
* @param mergedFile input file resulting from the merged step
* @param minifiedFile output file resulting from the minify step
* @throws IOException when the minify step fails
*/
abstract void minify(File mergedFile, File minifiedFile) throws IOException;
/**
* Logs compression gains.
*
* @param mergedFile input file resulting from the merged step
* @param minifiedFile output file resulting from the minify step
*/
void logCompressionGains(File mergedFile, File minifiedFile) {
try {
File temp = File.createTempFile(minifiedFile.getName(), ".gz");
try (InputStream in = new FileInputStream(minifiedFile);
OutputStream out = new FileOutputStream(temp);
GZIPOutputStream outGZIP = new GZIPOutputStream(out)) {
IOUtil.copy(in, outGZIP, bufferSize);
}
log.info("Uncompressed size: " + mergedFile.length() + " bytes.");
log.info("Compressed size: " + minifiedFile.length() + " bytes minified (" + temp.length()
+ " bytes gzipped).");
temp.deleteOnExit();
} catch (IOException e) {
log.debug("Failed to calculate the gzipped file size.", e);
}
}
/**
* Logs an addition of a new source file.
*
* @param finalFilename the final file name
* @param sourceFilename the source file name
* @throws FileNotFoundException when the given source file does not exist
*/
private void addNewSourceFile(String finalFilename, String sourceFilename) throws FileNotFoundException {
File sourceFile = new File(sourceDir, sourceFilename);
addNewSourceFile(finalFilename, sourceFile);
}
/**
* Logs an addition of a new source file.
*
* @param finalFilename the final file name
* @param sourceFile the source file
* @throws FileNotFoundException when the given source file does not exist
*/
private void addNewSourceFile(String finalFilename, File sourceFile) throws FileNotFoundException {
if (sourceFile.exists()) {
if (finalFilename.equalsIgnoreCase(sourceFile.getName())) {
log.warn("The source file [" + ((verbose) ? sourceFile.getPath() : sourceFile.getName())
+ "] has the same name as the final file.");
}
log.debug("Adding source file [" + ((verbose) ? sourceFile.getPath() : sourceFile.getName()) + "].");
files.add(sourceFile);
} else {
throw new FileNotFoundException("The source file ["
+ ((verbose) ? sourceFile.getPath() : sourceFile.getName()) + "] does not exist.");
}
}
/**
* Returns the files to copy. Default exclusions are used when the excludes list is empty.
*
* @param includes list of source files to include
* @param excludes list of source files to exclude
* @return the files to copy
*/
private List<File> getFilesToInclude(List<String> includes, List<String> excludes) {
List<File> includedFiles = new ArrayList<File>();
if (includes != null && !includes.isEmpty()) {
DirectoryScanner scanner = new DirectoryScanner();
scanner.setIncludes(includes.toArray(new String[0]));
scanner.setExcludes(excludes.toArray(new String[0]));
scanner.addDefaultExcludes();
scanner.setBasedir(sourceDir);
scanner.scan();
for (String includedFilename : scanner.getIncludedFiles()) {
includedFiles.add(new File(sourceDir, includedFilename));
}
Collections.sort(includedFiles, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return o1.getName().compareToIgnoreCase(o2.getName());
}
});
}
return includedFiles;
}
}