001/* 002 * Minify Maven Plugin 003 * https://github.com/samaxes/minify-maven-plugin 004 * 005 * Copyright (c) 2009 samaxes.com 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019package com.samaxes.maven.minify.plugin; 020 021import com.samaxes.maven.minify.common.SourceFilesEnumeration; 022import com.samaxes.maven.minify.common.YuiConfig; 023import com.samaxes.maven.minify.plugin.MinifyMojo.Engine; 024import org.apache.maven.plugin.logging.Log; 025import org.codehaus.plexus.util.DirectoryScanner; 026import org.codehaus.plexus.util.FileUtils; 027import org.codehaus.plexus.util.IOUtil; 028 029import java.io.*; 030import java.nio.charset.Charset; 031import java.util.ArrayList; 032import java.util.Collections; 033import java.util.Comparator; 034import java.util.List; 035import java.util.concurrent.Callable; 036import java.util.zip.GZIPOutputStream; 037 038/** 039 * Abstract class for merging and compressing a files list. 040 */ 041public abstract class ProcessFilesTask implements Callable<Object> { 042 043 public static final String TEMP_SUFFIX = ".tmp"; 044 045 protected final Log log; 046 047 protected final boolean verbose; 048 049 protected final Integer bufferSize; 050 051 protected final Charset charset; 052 053 protected final String suffix; 054 055 protected final boolean nosuffix; 056 057 protected final boolean skipMerge; 058 059 protected final boolean skipMinify; 060 061 protected final Engine engine; 062 063 protected final YuiConfig yuiConfig; 064 065 private final File sourceDir; 066 067 private final File targetDir; 068 069 private final String mergedFilename; 070 071 private final List<File> files = new ArrayList<>(); 072 073 private final boolean sourceFilesEmpty; 074 075 private final boolean sourceIncludesEmpty; 076 077 /** 078 * Task constructor. 079 * 080 * @param log Maven plugin log 081 * @param verbose display additional info 082 * @param bufferSize size of the buffer used to read source files 083 * @param charset if a character set is specified, a byte-to-char variant allows the encoding to be selected. 084 * Otherwise, only byte-to-byte operations are used 085 * @param suffix final file name suffix 086 * @param nosuffix whether to use a suffix for the minified file name or not 087 * @param skipMerge whether to skip the merge step or not 088 * @param skipMinify whether to skip the minify step or not 089 * @param webappSourceDir web resources source directory 090 * @param webappTargetDir web resources target directory 091 * @param inputDir directory containing source files 092 * @param sourceFiles list of source files to include 093 * @param sourceIncludes list of source files to include 094 * @param sourceExcludes list of source files to exclude 095 * @param outputDir directory to write the final file 096 * @param outputFilename the output file name 097 * @param engine minify processor engine selected 098 * @param yuiConfig YUI Compressor configuration 099 * @throws FileNotFoundException when the given source file does not exist 100 */ 101 public ProcessFilesTask(Log log, boolean verbose, Integer bufferSize, Charset charset, String suffix, 102 boolean nosuffix, boolean skipMerge, boolean skipMinify, String webappSourceDir, 103 String webappTargetDir, String inputDir, List<String> sourceFiles, 104 List<String> sourceIncludes, List<String> sourceExcludes, String outputDir, 105 String outputFilename, Engine engine, YuiConfig yuiConfig) throws FileNotFoundException { 106 this.log = log; 107 this.verbose = verbose; 108 this.bufferSize = bufferSize; 109 this.charset = charset; 110 this.suffix = suffix; 111 this.nosuffix = nosuffix; 112 this.skipMerge = skipMerge; 113 this.skipMinify = skipMinify; 114 this.engine = engine; 115 this.yuiConfig = yuiConfig; 116 117 this.sourceDir = new File(webappSourceDir + File.separator + inputDir); 118 this.targetDir = new File(webappTargetDir + File.separator + outputDir); 119 this.mergedFilename = outputFilename; 120 for (String sourceFilename : sourceFiles) { 121 addNewSourceFile(mergedFilename, sourceFilename); 122 } 123 for (File sourceInclude : getFilesToInclude(sourceIncludes, sourceExcludes)) { 124 if (!files.contains(sourceInclude)) { 125 addNewSourceFile(mergedFilename, sourceInclude); 126 } 127 } 128 this.sourceFilesEmpty = sourceFiles.isEmpty(); 129 this.sourceIncludesEmpty = sourceIncludes.isEmpty(); 130 } 131 132 /** 133 * Method executed by the thread. 134 * 135 * @throws IOException when the merge or minify steps fail 136 */ 137 @Override 138 public Object call() throws IOException { 139 synchronized (log) { 140 String fileType = (this instanceof ProcessCSSFilesTask) ? "CSS" : "JavaScript"; 141 log.info("Starting " + fileType + " task:"); 142 143 if (!targetDir.exists() && !targetDir.mkdirs()) { 144 throw new RuntimeException("Unable to create target directory for: " + targetDir); 145 } 146 147 if (!files.isEmpty()) { 148 if (skipMerge) { 149 log.info("Skipping the merge step..."); 150 String sourceBasePath = sourceDir.getAbsolutePath(); 151 152 for (File mergedFile : files) { 153 // Create folders to preserve sub-directory structure when only minifying 154 String originalPath = mergedFile.getAbsolutePath(); 155 String subPath = originalPath.substring(sourceBasePath.length(), 156 originalPath.lastIndexOf(File.separator)); 157 File targetPath = new File(targetDir.getAbsolutePath() + subPath); 158 if (!targetPath.exists() && !targetPath.mkdirs()) { 159 throw new RuntimeException("Unable to create target directory for: " + targetPath); 160 } 161 162 File minifiedFile = new File(targetPath, (nosuffix) ? mergedFile.getName() 163 : FileUtils.removeExtension(mergedFile.getName()) + suffix + "." + FileUtils.extension(mergedFile.getName())); 164 minify(mergedFile, minifiedFile); 165 } 166 } else if (skipMinify) { 167 File mergedFile = new File(targetDir, mergedFilename); 168 merge(mergedFile); 169 log.info("Skipping the minify step..."); 170 } else { 171 File mergedFile = new File(targetDir, (nosuffix) ? mergedFilename + TEMP_SUFFIX : mergedFilename); 172 merge(mergedFile); 173 File minifiedFile = new File(targetDir, (nosuffix) ? mergedFilename 174 : FileUtils.removeExtension(mergedFilename) + suffix + "." + FileUtils.extension(mergedFilename)); 175 minify(mergedFile, minifiedFile); 176 if (nosuffix) { 177 if (!mergedFile.delete()) { 178 mergedFile.deleteOnExit(); 179 } 180 } 181 } 182 log.info(""); 183 } else if (!sourceFilesEmpty || !sourceIncludesEmpty) { 184 // 'files' list will be empty if source file paths or names added to the project's POM are invalid. 185 log.error("No valid " + fileType + " source files found to process."); 186 } 187 } 188 189 return null; 190 } 191 192 /** 193 * Merges a list of source files. Create missing parent directories if needed. 194 * 195 * @param mergedFile output file resulting from the merged step 196 * @throws IOException when the merge step fails 197 */ 198 protected void merge(File mergedFile) throws IOException { 199 if (!mergedFile.getParentFile().exists() && !mergedFile.getParentFile().mkdirs()) { 200 throw new RuntimeException("Unable to create target directory for: " + mergedFile.getParentFile()); 201 } 202 203 try (InputStream sequence = new SequenceInputStream(new SourceFilesEnumeration(log, files, verbose)); 204 OutputStream out = new FileOutputStream(mergedFile); 205 InputStreamReader sequenceReader = new InputStreamReader(sequence, charset); 206 OutputStreamWriter outWriter = new OutputStreamWriter(out, charset)) { 207 log.info("Creating the merged file [" + (verbose ? mergedFile.getPath() : mergedFile.getName()) + "]."); 208 209 IOUtil.copy(sequenceReader, outWriter, bufferSize); 210 } catch (IOException e) { 211 log.error("Failed to concatenate files.", e); 212 throw e; 213 } 214 } 215 216 /** 217 * Minifies a source file. Create missing parent directories if needed. 218 * 219 * @param mergedFile input file resulting from the merged step 220 * @param minifiedFile output file resulting from the minify step 221 * @throws IOException when the minify step fails 222 */ 223 abstract void minify(File mergedFile, File minifiedFile) throws IOException; 224 225 /** 226 * Logs compression gains. 227 * 228 * @param mergedFile input file resulting from the merged step 229 * @param minifiedFile output file resulting from the minify step 230 */ 231 void logCompressionGains(File mergedFile, File minifiedFile) { 232 try { 233 File temp = File.createTempFile(minifiedFile.getName(), ".gz"); 234 235 try (InputStream in = new FileInputStream(minifiedFile); 236 OutputStream out = new FileOutputStream(temp); 237 GZIPOutputStream outGZIP = new GZIPOutputStream(out)) { 238 IOUtil.copy(in, outGZIP, bufferSize); 239 } 240 241 log.info("Uncompressed size: " + mergedFile.length() + " bytes."); 242 log.info("Compressed size: " + minifiedFile.length() + " bytes minified (" + temp.length() 243 + " bytes gzipped)."); 244 245 temp.deleteOnExit(); 246 } catch (IOException e) { 247 log.debug("Failed to calculate the gzipped file size.", e); 248 } 249 } 250 251 /** 252 * Logs an addition of a new source file. 253 * 254 * @param finalFilename the final file name 255 * @param sourceFilename the source file name 256 * @throws FileNotFoundException when the given source file does not exist 257 */ 258 private void addNewSourceFile(String finalFilename, String sourceFilename) throws FileNotFoundException { 259 File sourceFile = new File(sourceDir, sourceFilename); 260 261 addNewSourceFile(finalFilename, sourceFile); 262 } 263 264 /** 265 * Logs an addition of a new source file. 266 * 267 * @param finalFilename the final file name 268 * @param sourceFile the source file 269 * @throws FileNotFoundException when the given source file does not exist 270 */ 271 private void addNewSourceFile(String finalFilename, File sourceFile) throws FileNotFoundException { 272 if (sourceFile.exists()) { 273 if (finalFilename.equalsIgnoreCase(sourceFile.getName())) { 274 log.warn("The source file [" + (verbose ? sourceFile.getPath() : sourceFile.getName()) 275 + "] has the same name as the final file."); 276 } 277 log.debug("Adding source file [" + (verbose ? sourceFile.getPath() : sourceFile.getName()) + "]."); 278 files.add(sourceFile); 279 } else { 280 throw new FileNotFoundException("The source file [" 281 + (verbose ? sourceFile.getPath() : sourceFile.getName()) + "] does not exist."); 282 } 283 } 284 285 /** 286 * Returns the files to copy. Default exclusions are used when the excludes list is empty. 287 * 288 * @param includes list of source files to include 289 * @param excludes list of source files to exclude 290 * @return the files to copy 291 */ 292 private List<File> getFilesToInclude(List<String> includes, List<String> excludes) { 293 List<File> includedFiles = new ArrayList<>(); 294 295 if (includes != null && !includes.isEmpty()) { 296 DirectoryScanner scanner = new DirectoryScanner(); 297 298 scanner.setIncludes(includes.toArray(new String[includes.size()])); 299 scanner.setExcludes(excludes.toArray(new String[excludes.size()])); 300 scanner.addDefaultExcludes(); 301 scanner.setBasedir(sourceDir); 302 scanner.scan(); 303 304 for (String includedFilename : scanner.getIncludedFiles()) { 305 includedFiles.add(new File(sourceDir, includedFilename)); 306 } 307 308 Collections.sort(includedFiles, new Comparator<File>() { 309 @Override 310 public int compare(File o1, File o2) { 311 return o1.getName().compareToIgnoreCase(o2.getName()); 312 } 313 }); 314 } 315 316 return includedFiles; 317 } 318}