View Javadoc
1   /*
2    *    Copyright 2009-2024 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package com.googlecode.htmlcompressor;
17  
18  import com.google.javascript.jscomp.CompilationLevel;
19  import com.google.javascript.jscomp.SourceFile;
20  import com.googlecode.htmlcompressor.analyzer.HtmlAnalyzer;
21  import com.googlecode.htmlcompressor.compressor.ClosureJavaScriptCompressor;
22  import com.googlecode.htmlcompressor.compressor.Compressor;
23  import com.googlecode.htmlcompressor.compressor.HtmlCompressor;
24  import com.googlecode.htmlcompressor.compressor.XmlCompressor;
25  
26  import java.io.BufferedReader;
27  import java.io.Closeable;
28  import java.io.File;
29  import java.io.FileFilter;
30  import java.io.FileInputStream;
31  import java.io.FileOutputStream;
32  import java.io.IOException;
33  import java.io.InputStreamReader;
34  import java.io.OutputStreamWriter;
35  import java.io.Writer;
36  import java.net.URL;
37  import java.nio.charset.Charset;
38  import java.nio.charset.StandardCharsets;
39  import java.util.ArrayDeque;
40  import java.util.ArrayList;
41  import java.util.HashMap;
42  import java.util.List;
43  import java.util.Locale;
44  import java.util.Map;
45  import java.util.regex.Matcher;
46  import java.util.regex.Pattern;
47  import java.util.regex.PatternSyntaxException;
48  
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import jargs.gnu.CmdLineParser;
53  import jargs.gnu.CmdLineParser.Option;
54  import jargs.gnu.CmdLineParser.OptionException;
55  
56  /**
57   * Wrapper for HTML and XML compressor classes that allows using them from a command line.
58   * <p>
59   * Usage: <code>java -jar htmlcompressor.jar [options] [input]</code>
60   * <p>
61   * To view a list of all available parameters please run with <code>-?</code> option:
62   * <p>
63   * <code>java -jar htmlcompressor.jar -?</code>
64   *
65   * @author <a href="mailto:serg472@gmail.com">Sergiy Kovalchuk</a>
66   */
67  public class CmdLineCompressor {
68  
69      /** The Constant logger. */
70      private static final Logger logger = LoggerFactory.getLogger(CmdLineCompressor.class);
71  
72      /** The Constant urlPattern. */
73      private static final Pattern urlPattern = Pattern.compile("^https?://.*$", Pattern.CASE_INSENSITIVE);
74  
75      /** The help opt. */
76      private boolean helpOpt;
77  
78      /** The analyze opt. */
79      private boolean analyzeOpt;
80  
81      /** The charset opt. */
82      private Charset charsetOpt;
83  
84      /** The output filename opt. */
85      private String outputFilenameOpt;
86  
87      /** The patterns filename opt. */
88      private String patternsFilenameOpt;
89  
90      /** The type opt. */
91      private String typeOpt;
92  
93      /** The filemask opt. */
94      private String filemaskOpt;
95  
96      /** The recursive opt. */
97      private boolean recursiveOpt;
98  
99      /** The preserve comments opt. */
100     private boolean preserveCommentsOpt;
101 
102     /** The preserve intertag spaces opt. */
103     private boolean preserveIntertagSpacesOpt;
104 
105     /** The preserve multi spaces opt. */
106     private boolean preserveMultiSpacesOpt;
107 
108     /** The remove intertag spaces opt. */
109     private boolean removeIntertagSpacesOpt;
110 
111     /** The remove quotes opt. */
112     private boolean removeQuotesOpt;
113 
114     /** The remove surrounding spaces opt. */
115     private String removeSurroundingSpacesOpt;
116 
117     /** The preserve line breaks opt. */
118     private boolean preserveLineBreaksOpt;
119 
120     /** The preserve php tags opt. */
121     private boolean preservePhpTagsOpt;
122 
123     /** The preserve server script tags opt. */
124     private boolean preserveServerScriptTagsOpt;
125 
126     /** The preserve ssi tags opt. */
127     private boolean preserveSsiTagsOpt;
128 
129     /** The compress js opt. */
130     private boolean compressJsOpt;
131 
132     /** The compress css opt. */
133     private boolean compressCssOpt;
134 
135     /** The js compressor opt. */
136     private String jsCompressorOpt;
137 
138     /** The simple doctype opt. */
139     private boolean simpleDoctypeOpt;
140 
141     /** The remove script attributes opt. */
142     private boolean removeScriptAttributesOpt;
143 
144     /** The remove style attributes opt. */
145     private boolean removeStyleAttributesOpt;
146 
147     /** The remove link attributes opt. */
148     private boolean removeLinkAttributesOpt;
149 
150     /** The remove form attributes opt. */
151     private boolean removeFormAttributesOpt;
152 
153     /** The remove input attributes opt. */
154     private boolean removeInputAttributesOpt;
155 
156     /** The simple boolean attributes opt. */
157     private boolean simpleBooleanAttributesOpt;
158 
159     /** The remove java script protocol opt. */
160     private boolean removeJavaScriptProtocolOpt;
161 
162     /** The remove http protocol opt. */
163     private boolean removeHttpProtocolOpt;
164 
165     /** The remove https protocol opt. */
166     private boolean removeHttpsProtocolOpt;
167 
168     /** The nomunge opt. */
169     private boolean nomungeOpt;
170 
171     /** The linebreak opt. */
172     private int linebreakOpt;
173 
174     /** The preserve semi opt. */
175     private boolean preserveSemiOpt;
176 
177     /** The disable optimizations opt. */
178     private boolean disableOptimizationsOpt;
179 
180     /** The closure opt level opt. */
181     private String closureOptLevelOpt;
182 
183     /** The closure custom externs only opt. */
184     private boolean closureCustomExternsOnlyOpt;
185 
186     /** The closure externs opt. */
187     private List<String> closureExternsOpt;
188 
189     /** The file args opt. */
190     private List<String> fileArgsOpt;
191 
192     /**
193      * Instantiates a new cmd line compressor.
194      *
195      * @param args
196      *            the args
197      */
198     public CmdLineCompressor(String[] args) {
199         CmdLineParser parser = new CmdLineParser();
200 
201         Option helpOption = parser.addBooleanOption('h', "help");
202         Option helpOptAlt = parser.addBooleanOption('?', "help_alt");
203         Option analyzeOption = parser.addBooleanOption('a', "analyze");
204         Option recursiveOption = parser.addBooleanOption('r', "recursive");
205         Option charsetOption = parser.addStringOption('c', "charset");
206         Option outputFilenameOption = parser.addStringOption('o', "output");
207         Option patternsFilenameOption = parser.addStringOption('p', "preserve");
208         Option typeOption = parser.addStringOption('t', "type");
209         Option filemaskOption = parser.addStringOption('m', "mask");
210         Option preserveCommentsOption = parser.addBooleanOption("preserve-comments");
211         Option preserveIntertagSpacesOption = parser.addBooleanOption("preserve-intertag-spaces");
212         Option preserveMultiSpacesOption = parser.addBooleanOption("preserve-multi-spaces");
213         Option removeIntertagSpacesOption = parser.addBooleanOption("remove-intertag-spaces");
214         Option removeSurroundingSpacesOption = parser.addStringOption("remove-surrounding-spaces");
215         Option removeQuotesOption = parser.addBooleanOption("remove-quotes");
216         Option preserveLineBreaksOption = parser.addBooleanOption("preserve-line-breaks");
217         Option preservePhpTagsOption = parser.addBooleanOption("preserve-php");
218         Option preserveServerScriptTagsOption = parser.addBooleanOption("preserve-server-script");
219         Option preserveSsiTagsOption = parser.addBooleanOption("preserve-ssi");
220         Option compressJsOption = parser.addBooleanOption("compress-js");
221         Option compressCssOption = parser.addBooleanOption("compress-css");
222         Option jsCompressorOption = parser.addStringOption("js-compressor");
223 
224         Option simpleDoctypeOption = parser.addBooleanOption("simple-doctype");
225         Option removeScriptAttributesOption = parser.addBooleanOption("remove-script-attr");
226         Option removeStyleAttributesOption = parser.addBooleanOption("remove-style-attr");
227         Option removeLinkAttributesOption = parser.addBooleanOption("remove-link-attr");
228         Option removeFormAttributesOption = parser.addBooleanOption("remove-form-attr");
229         Option removeInputAttributesOption = parser.addBooleanOption("remove-input-attr");
230         Option simpleBooleanAttributesOption = parser.addBooleanOption("simple-bool-attr");
231         Option removeJavaScriptProtocolOption = parser.addBooleanOption("remove-js-protocol");
232         Option removeHttpProtocolOption = parser.addBooleanOption("remove-http-protocol");
233         Option removeHttpsProtocolOption = parser.addBooleanOption("remove-https-protocol");
234 
235         Option nomungeOption = parser.addBooleanOption("nomunge");
236         Option linebreakOption = parser.addStringOption("line-break");
237         Option preserveSemiOption = parser.addBooleanOption("preserve-semi");
238         Option disableOptimizationsOption = parser.addBooleanOption("disable-optimizations");
239 
240         Option closureOptLevelOption = parser.addStringOption("closure-opt-level");
241         Option closureCustomExternsOnlyOption = parser.addBooleanOption("closure-custom-externs-only");
242         Option closureExternsOption = parser.addStringOption("closure-externs");
243 
244         try {
245             parser.parse(args);
246 
247             this.helpOpt = (Boolean) parser.getOptionValue(helpOption, false)
248                     || (Boolean) parser.getOptionValue(helpOptAlt, false);
249             this.analyzeOpt = (Boolean) parser.getOptionValue(analyzeOption, false);
250             this.recursiveOpt = (Boolean) parser.getOptionValue(recursiveOption, false);
251             this.charsetOpt = Charset.forName((String) parser.getOptionValue(charsetOption, "UTF-8"));
252             this.outputFilenameOpt = (String) parser.getOptionValue(outputFilenameOption);
253             this.patternsFilenameOpt = (String) parser.getOptionValue(patternsFilenameOption);
254             this.typeOpt = (String) parser.getOptionValue(typeOption);
255             this.filemaskOpt = (String) parser.getOptionValue(filemaskOption);
256             this.preserveCommentsOpt = (Boolean) parser.getOptionValue(preserveCommentsOption, false);
257             this.preserveIntertagSpacesOpt = (Boolean) parser.getOptionValue(preserveIntertagSpacesOption, false);
258             this.preserveMultiSpacesOpt = (Boolean) parser.getOptionValue(preserveMultiSpacesOption, false);
259             this.removeIntertagSpacesOpt = (Boolean) parser.getOptionValue(removeIntertagSpacesOption, false);
260             this.removeQuotesOpt = (Boolean) parser.getOptionValue(removeQuotesOption, false);
261             this.preserveLineBreaksOpt = (Boolean) parser.getOptionValue(preserveLineBreaksOption, false);
262             this.preservePhpTagsOpt = (Boolean) parser.getOptionValue(preservePhpTagsOption, false);
263             this.preserveServerScriptTagsOpt = (Boolean) parser.getOptionValue(preserveServerScriptTagsOption, false);
264             this.preserveSsiTagsOpt = (Boolean) parser.getOptionValue(preserveSsiTagsOption, false);
265             this.compressJsOpt = (Boolean) parser.getOptionValue(compressJsOption, false);
266             this.compressCssOpt = (Boolean) parser.getOptionValue(compressCssOption, false);
267             this.jsCompressorOpt = (String) parser.getOptionValue(jsCompressorOption, HtmlCompressor.JS_COMPRESSOR_YUI);
268 
269             this.simpleDoctypeOpt = (Boolean) parser.getOptionValue(simpleDoctypeOption, false);
270             this.removeScriptAttributesOpt = (Boolean) parser.getOptionValue(removeScriptAttributesOption, false);
271             this.removeStyleAttributesOpt = (Boolean) parser.getOptionValue(removeStyleAttributesOption, false);
272             this.removeLinkAttributesOpt = (Boolean) parser.getOptionValue(removeLinkAttributesOption, false);
273             this.removeFormAttributesOpt = (Boolean) parser.getOptionValue(removeFormAttributesOption, false);
274             this.removeInputAttributesOpt = (Boolean) parser.getOptionValue(removeInputAttributesOption, false);
275             this.simpleBooleanAttributesOpt = (Boolean) parser.getOptionValue(simpleBooleanAttributesOption, false);
276             this.removeJavaScriptProtocolOpt = (Boolean) parser.getOptionValue(removeJavaScriptProtocolOption, false);
277             this.removeHttpProtocolOpt = (Boolean) parser.getOptionValue(removeHttpProtocolOption, false);
278             this.removeHttpsProtocolOpt = (Boolean) parser.getOptionValue(removeHttpsProtocolOption, false);
279 
280             this.nomungeOpt = (Boolean) parser.getOptionValue(nomungeOption, false);
281             this.linebreakOpt = (Integer) parser.getOptionValue(linebreakOption, -1);
282             this.preserveSemiOpt = (Boolean) parser.getOptionValue(preserveSemiOption, false);
283             this.disableOptimizationsOpt = (Boolean) parser.getOptionValue(disableOptimizationsOption, false);
284 
285             this.closureOptLevelOpt = (String) parser.getOptionValue(closureOptLevelOption,
286                     ClosureJavaScriptCompressor.COMPILATION_LEVEL_SIMPLE);
287             this.closureCustomExternsOnlyOpt = (Boolean) parser.getOptionValue(closureCustomExternsOnlyOption, false);
288 
289             this.closureExternsOpt = parser.getOptionValues(closureExternsOption);
290 
291             this.removeSurroundingSpacesOpt = (String) parser.getOptionValue(removeSurroundingSpacesOption);
292             if (this.removeSurroundingSpacesOpt != null) {
293                 if ("min".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
294                     this.removeSurroundingSpacesOpt = HtmlCompressor.BLOCK_TAGS_MIN;
295                 } else if ("max".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
296                     this.removeSurroundingSpacesOpt = HtmlCompressor.BLOCK_TAGS_MAX;
297                 } else if ("all".equalsIgnoreCase(this.removeSurroundingSpacesOpt)) {
298                     this.removeSurroundingSpacesOpt = HtmlCompressor.ALL_TAGS;
299                 }
300             }
301 
302             // input file
303             this.fileArgsOpt = parser.getRemainingArgs();
304 
305             // charset
306             this.charsetOpt = Charset.isSupported(this.charsetOpt.name()) ? this.charsetOpt : StandardCharsets.UTF_8;
307 
308             // look for "/?"
309             for (int i = 0; i < args.length; i++) {
310                 if ("/?".equals(args[i])) {
311                     this.helpOpt = true;
312                     break;
313                 }
314             }
315 
316         } catch (OptionException e) {
317             logger.info("{}", e.getMessage());
318             logger.trace("", e);
319             printUsage();
320         }
321 
322     }
323 
324     /**
325      * The main method.
326      *
327      * @param args
328      *            the arguments
329      */
330     public static void main(String[] args) {
331         CmdLineCompressor cmdLineCompressor = new CmdLineCompressor(args);
332         cmdLineCompressor.process();
333     }
334 
335     /**
336      * Process.
337      */
338     public void process() {
339         try {
340 
341             // help
342             if (helpOpt) {
343                 printUsage();
344                 return;
345             }
346 
347             // type
348             String type = typeOpt;
349             if (type != null && !"html".equalsIgnoreCase(type) && !"xml".equalsIgnoreCase(type)) {
350                 throw new IllegalArgumentException("Unknown type: " + type);
351             }
352 
353             if (fileArgsOpt.isEmpty()) {
354                 // html by default for stdin
355                 if (type == null) {
356                     type = "html";
357                 }
358             } else if (type == null) {
359                 // detect type from extension
360                 if (fileArgsOpt.get(0).toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
361                     type = "xml";
362                 } else {
363                     type = "html";
364                 }
365             }
366 
367             if (analyzeOpt) {
368                 // analyzer mode
369                 HtmlAnalyzer analyzer = new HtmlAnalyzer(
370                         HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt)
371                                 ? HtmlCompressor.JS_COMPRESSOR_CLOSURE : HtmlCompressor.JS_COMPRESSOR_YUI);
372                 analyzer.analyze(readResource(buildReader(fileArgsOpt.isEmpty() ? null : fileArgsOpt.get(0))));
373             } else {
374                 // compression mode
375                 Compressor compressor = "xml".equalsIgnoreCase(type) ? createXmlCompressor() : createHtmlCompressor();
376                 Map<String, String> ioMap = buildInputOutputMap();
377                 for (Map.Entry<String, String> entry : ioMap.entrySet()) {
378                     writeResource(compressor.compress(readResource(buildReader(entry.getKey()))),
379                             buildWriter(entry.getValue()));
380                 }
381             }
382 
383         } catch (NoClassDefFoundError e) {
384             if (HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt)) {
385                 logger.info("ERROR: For JavaScript compression using Google Closure Compiler\n"
386                         + "additional jar file called compiler.jar must be present\n"
387                         + "in the same directory as HtmlCompressor jar");
388             } else {
389                 logger.info("ERROR: For CSS or JavaScript compression using YUICompressor additional jar file \n"
390                         + "called yuicompressor.jar must be present\n" + "in the same directory as HtmlCompressor jar");
391             }
392             logger.trace("", e);
393         } catch (OptionException e) {
394             logger.info("{}", e.getMessage());
395             logger.trace("", e);
396             printUsage();
397         } catch (IOException | IllegalArgumentException e) {
398             logger.info("{}", e.getMessage());
399             logger.trace("", e);
400         }
401 
402     }
403 
404     /**
405      * Creates the html compressor.
406      *
407      * @return the compressor
408      *
409      * @throws OptionException
410      *             the option exception
411      */
412     private Compressor createHtmlCompressor() throws OptionException {
413 
414         boolean useClosureCompressor = HtmlCompressor.JS_COMPRESSOR_CLOSURE.equalsIgnoreCase(jsCompressorOpt);
415 
416         // custom preserve patterns
417         List<Pattern> preservePatterns = new ArrayList<>();
418 
419         // predefined
420         if (preservePhpTagsOpt) {
421             preservePatterns.add(HtmlCompressor.PHP_TAG_PATTERN);
422         }
423 
424         if (preserveServerScriptTagsOpt) {
425             preservePatterns.add(HtmlCompressor.SERVER_SCRIPT_TAG_PATTERN);
426         }
427 
428         if (preserveSsiTagsOpt) {
429             preservePatterns.add(HtmlCompressor.SERVER_SIDE_INCLUDE_PATTERN);
430         }
431 
432         if (patternsFilenameOpt != null) {
433 
434             try (FileInputStream stream = new FileInputStream(patternsFilenameOpt);
435                     BufferedReader patternsIn = new BufferedReader(new InputStreamReader(stream, charsetOpt))) {
436 
437                 String line = null;
438                 while ((line = patternsIn.readLine()) != null) {
439                     if (line.length() > 0) {
440                         try {
441                             preservePatterns.add(Pattern.compile(line));
442                         } catch (PatternSyntaxException e) {
443                             logger.trace("", e);
444                             throw new IllegalArgumentException(
445                                     "Regular expression compilation error: " + e.getMessage());
446                         }
447                     }
448                 }
449             } catch (IOException e) {
450                 logger.trace("", e);
451                 throw new IllegalArgumentException("Unable to read custom pattern definitions file: " + e.getMessage());
452             }
453         }
454 
455         // set compressor options
456         HtmlCompressor htmlCompressor = new HtmlCompressor();
457 
458         htmlCompressor.setRemoveComments(!preserveCommentsOpt);
459         htmlCompressor.setRemoveMultiSpaces(!preserveMultiSpacesOpt);
460         htmlCompressor.setRemoveIntertagSpaces(removeIntertagSpacesOpt);
461         htmlCompressor.setRemoveQuotes(removeQuotesOpt);
462         htmlCompressor.setPreserveLineBreaks(preserveLineBreaksOpt);
463         htmlCompressor.setCompressJavaScript(compressJsOpt);
464         htmlCompressor.setCompressCss(compressCssOpt);
465 
466         htmlCompressor.setSimpleDoctype(simpleDoctypeOpt);
467         htmlCompressor.setRemoveScriptAttributes(removeScriptAttributesOpt);
468         htmlCompressor.setRemoveStyleAttributes(removeStyleAttributesOpt);
469         htmlCompressor.setRemoveLinkAttributes(removeLinkAttributesOpt);
470         htmlCompressor.setRemoveFormAttributes(removeFormAttributesOpt);
471         htmlCompressor.setRemoveInputAttributes(removeInputAttributesOpt);
472         htmlCompressor.setSimpleBooleanAttributes(simpleBooleanAttributesOpt);
473         htmlCompressor.setRemoveJavaScriptProtocol(removeJavaScriptProtocolOpt);
474         htmlCompressor.setRemoveHttpProtocol(removeHttpProtocolOpt);
475         htmlCompressor.setRemoveHttpsProtocol(removeHttpsProtocolOpt);
476         htmlCompressor.setRemoveSurroundingSpaces(removeSurroundingSpacesOpt);
477 
478         htmlCompressor.setPreservePatterns(preservePatterns);
479 
480         htmlCompressor.setYuiJsNoMunge(nomungeOpt);
481         htmlCompressor.setYuiJsPreserveAllSemiColons(preserveSemiOpt);
482         htmlCompressor.setYuiJsDisableOptimizations(disableOptimizationsOpt);
483         htmlCompressor.setYuiJsLineBreak(linebreakOpt);
484         htmlCompressor.setYuiCssLineBreak(linebreakOpt);
485 
486         // switch js compressor to closure
487         if (compressJsOpt && useClosureCompressor) {
488             ClosureJavaScriptCompressor closureCompressor = new ClosureJavaScriptCompressor();
489 
490             if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_ADVANCED)) {
491                 closureCompressor.setCompilationLevel(CompilationLevel.ADVANCED_OPTIMIZATIONS);
492                 closureCompressor.setCustomExternsOnly(closureCustomExternsOnlyOpt);
493 
494                 // get externs
495                 if (!closureExternsOpt.isEmpty()) {
496                     List<SourceFile> externs = new ArrayList<>();
497                     for (String externFile : closureExternsOpt) {
498                         externs.add(SourceFile.fromFile(externFile));
499                     }
500                     closureCompressor.setExterns(externs);
501                 }
502             } else if (closureOptLevelOpt.equalsIgnoreCase(ClosureJavaScriptCompressor.COMPILATION_LEVEL_WHITESPACE)) {
503                 closureCompressor.setCompilationLevel(CompilationLevel.WHITESPACE_ONLY);
504             } else {
505                 closureCompressor.setCompilationLevel(CompilationLevel.SIMPLE_OPTIMIZATIONS);
506             }
507 
508             htmlCompressor.setJavaScriptCompressor(closureCompressor);
509         }
510 
511         return htmlCompressor;
512     }
513 
514     /**
515      * Creates the xml compressor.
516      *
517      * @return the compressor
518      *
519      * @throws IllegalArgumentException
520      *             the illegal argument exception
521      * @throws OptionException
522      *             the option exception
523      */
524     private Compressor createXmlCompressor() throws IllegalArgumentException, OptionException {
525         XmlCompressor xmlCompressor = new XmlCompressor();
526         xmlCompressor.setRemoveComments(!preserveCommentsOpt);
527         xmlCompressor.setRemoveIntertagSpaces(!preserveIntertagSpacesOpt);
528 
529         return xmlCompressor;
530     }
531 
532     /**
533      * Builds the input output map.
534      *
535      * @return the map
536      *
537      * @throws IllegalArgumentException
538      *             the illegal argument exception
539      * @throws IOException
540      *             Signals that an I/O exception has occurred.
541      */
542     private Map<String, String> buildInputOutputMap() throws IllegalArgumentException, IOException {
543         Map<String, String> map = new HashMap<>();
544 
545         File outpuFile = null;
546         if (outputFilenameOpt != null) {
547             outpuFile = new File(outputFilenameOpt);
548 
549             // make dirs
550             if (outputFilenameOpt.endsWith("/") || outputFilenameOpt.endsWith("\\")) {
551                 outpuFile.mkdirs();
552             } else {
553                 new File(outpuFile.getCanonicalFile().getParent()).mkdirs();
554             }
555         }
556 
557         if (fileArgsOpt.size() > 1 && (outpuFile == null || !outpuFile.isDirectory())) {
558             throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
559         }
560 
561         if (fileArgsOpt.isEmpty()) {
562             map.put(null, outputFilenameOpt);
563         } else {
564             for (int i = 0; i < fileArgsOpt.size(); i++) {
565                 if (!urlPattern.matcher(fileArgsOpt.get(i)).matches()) {
566                     File inputFile = new File(fileArgsOpt.get(i));
567                     if (inputFile.isDirectory()) {
568                         // is dir
569                         if (outpuFile != null && outpuFile.isDirectory()) {
570                             if (!recursiveOpt) {
571                                 // non-recursive
572                                 for (File file : inputFile
573                                         .listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, false))) {
574                                     if (!file.isDirectory()) {
575                                         String from = file.getCanonicalPath();
576                                         String to = from.replaceFirst(escRegEx(inputFile.getCanonicalPath()),
577                                                 Matcher.quoteReplacement(outpuFile.getCanonicalPath()));
578                                         map.put(from, to);
579                                     }
580                                 }
581                             } else {
582                                 // recursive
583                                 ArrayDeque<File> fileStack = new ArrayDeque<>();
584                                 fileStack.push(inputFile);
585                                 while (!fileStack.isEmpty()) {
586                                     File child = fileStack.pop();
587                                     if (child.isDirectory()) {
588                                         for (File f : child
589                                                 .listFiles(new CompressorFileFilter(typeOpt, filemaskOpt, true))) {
590                                             fileStack.push(f);
591                                         }
592                                     } else if (child.isFile()) {
593                                         String from = child.getCanonicalPath();
594                                         String to = from.replaceFirst(escRegEx(inputFile.getCanonicalPath()),
595                                                 Matcher.quoteReplacement(outpuFile.getCanonicalPath()));
596                                         map.put(from, to);
597                                         // make dirs
598                                         new File(new File(to).getCanonicalFile().getParent()).mkdirs();
599                                     }
600                                 }
601                             }
602                         } else {
603                             throw new IllegalArgumentException("Output must be a directory and end with a slash (/)");
604                         }
605                     } else {
606                         // is file
607                         if (outpuFile != null && outpuFile.isDirectory()) {
608                             String from = inputFile.getCanonicalPath();
609                             String to = from.replaceFirst(
610                                     escRegEx(inputFile.getCanonicalFile().getParentFile().getCanonicalPath()),
611                                     Matcher.quoteReplacement(outpuFile.getCanonicalPath()));
612                             map.put(fileArgsOpt.get(i), to);
613                         } else {
614                             map.put(fileArgsOpt.get(i), outputFilenameOpt);
615                         }
616 
617                     }
618                 } else {
619                     // is url
620                     if (fileArgsOpt.size() == 1 && (outpuFile == null || !outpuFile.isDirectory())) {
621                         map.put(fileArgsOpt.get(i), outputFilenameOpt);
622                     } else {
623                         throw new IllegalArgumentException(
624                                 "Input URL should be single and cannot have directory as output");
625                     }
626                 }
627             }
628         }
629 
630         return map;
631     }
632 
633     /**
634      * Builds the reader.
635      *
636      * @param filename
637      *            the filename
638      *
639      * @return the buffered reader
640      *
641      * @throws IOException
642      *             Signals that an I/O exception has occurred.
643      */
644     private BufferedReader buildReader(String filename) throws IOException {
645         if (filename == null) {
646             return new BufferedReader(new InputStreamReader(System.in, charsetOpt));
647         } else if (urlPattern.matcher(filename).matches()) {
648             return new BufferedReader(
649                     new InputStreamReader(new URL(filename).openConnection().getInputStream(), charsetOpt));
650         } else {
651             return new BufferedReader(new InputStreamReader(new FileInputStream(filename), charsetOpt));
652         }
653     }
654 
655     /**
656      * Builds the writer.
657      *
658      * @param filename
659      *            the filename
660      *
661      * @return the writer
662      *
663      * @throws IOException
664      *             Signals that an I/O exception has occurred.
665      */
666     private Writer buildWriter(String filename) throws IOException {
667         if (filename == null) {
668             return new OutputStreamWriter(System.out, charsetOpt);
669         } else {
670             return new OutputStreamWriter(new FileOutputStream(filename), charsetOpt);
671         }
672     }
673 
674     /**
675      * Read resource.
676      *
677      * @param input
678      *            the input
679      *
680      * @return the string
681      *
682      * @throws IOException
683      *             Signals that an I/O exception has occurred.
684      */
685     private String readResource(BufferedReader input) throws IOException {
686         StringBuilder source = new StringBuilder();
687         try {
688             String line = null;
689             while ((line = input.readLine()) != null) {
690                 source.append(line);
691                 source.append(System.getProperty("line.separator"));
692             }
693         } finally {
694             closeStream(input);
695         }
696         return source.toString();
697     }
698 
699     /**
700      * Write resource.
701      *
702      * @param content
703      *            the content
704      * @param output
705      *            the output
706      *
707      * @throws IOException
708      *             Signals that an I/O exception has occurred.
709      */
710     private void writeResource(String content, Writer output) throws IOException {
711         try {
712             output.write(content);
713         } finally {
714             closeStream(output);
715         }
716     }
717 
718     /**
719      * Close stream.
720      *
721      * @param stream
722      *            the stream
723      */
724     private void closeStream(Closeable stream) {
725         if (stream != null) {
726             try {
727                 stream.close();
728             } catch (IOException e) {
729                 logger.trace("", e);
730             }
731         }
732     }
733 
734     /**
735      * Esc reg ex.
736      *
737      * @param inStr
738      *            the in str
739      *
740      * @return the string
741      */
742     private String escRegEx(String inStr) {
743         return inStr.replaceAll("([\\\\*+\\[\\](){}\\$.?\\^|])", "\\\\$1");
744     }
745 
746     /**
747      * Prints the usage.
748      */
749     private void printUsage() {
750         logger.info("Usage: java -jar htmlcompressor.jar [options] [input]\n\n"
751 
752                 + "[input]                        URL, filename, directory, or space separated list\n"
753                 + "                               of files and directories to compress.\n"
754                 + "                               If none provided reads from <stdin>\n\n"
755 
756                 + "Global Options:\n" + " -?, /?, -h, --help            Displays this help screen\n"
757                 + " -t, --type <html|xml>         If not provided autodetects from file extension\n"
758                 + " -r, --recursive               Process files inside subdirectories\n"
759                 + " -c, --charset <charset>       Charset for reading files, UTF-8 by default\n"
760                 + " -m, --mask <filemask>         Filter input files inside directories by mask\n"
761                 + " -o, --output <path>           Filename or directory for compression results.\n"
762                 + "                               If none provided outputs result to <stdout>\n"
763                 + " -a, --analyze                 Tries different settings and displays report.\n"
764                 + "                               All settings except --js-compressor are ignored\n\n"
765 
766                 + "XML Compression Options:\n" + " --preserve-comments           Preserve comments\n"
767                 + " --preserve-intertag-spaces    Preserve intertag spaces\n\n"
768 
769                 + "HTML Compression Options:\n" + " --preserve-comments           Preserve comments\n"
770                 + " --preserve-multi-spaces       Preserve multiple spaces\n"
771                 + " --preserve-line-breaks        Preserve line breaks\n"
772                 + " --remove-intertag-spaces      Remove intertag spaces\n"
773                 + " --remove-quotes               Remove unneeded quotes\n"
774                 + " --simple-doctype              Change doctype to <!DOCTYPE html>\n"
775                 + " --remove-style-attr           Remove TYPE attribute from STYLE tags\n"
776                 + " --remove-link-attr            Remove TYPE attribute from LINK tags\n"
777                 + " --remove-script-attr          Remove TYPE and LANGUAGE from SCRIPT tags\n"
778                 + " --remove-form-attr            Remove METHOD=\"GET\" from FORM tags\n"
779                 + " --remove-input-attr           Remove TYPE=\"TEXT\" from INPUT tags\n"
780                 + " --simple-bool-attr            Remove values from boolean tag attributes\n"
781                 + " --remove-js-protocol          Remove \"javascript:\" from inline event handlers\n"
782                 + " --remove-http-protocol        Remove \"http:\" from tag attributes\n"
783                 + " --remove-https-protocol       Remove \"https:\" from tag attributes\n"
784                 + " --remove-surrounding-spaces <min|max|all|custom_list>\n"
785                 + "                               Predefined or custom comma separated list of tags\n"
786                 + " --compress-js                 Enable inline JavaScript compression\n"
787                 + " --compress-css                Enable inline CSS compression using YUICompressor\n"
788                 + " --js-compressor <yui|closure> Switch inline JavaScript compressor between\n"
789                 + "                               YUICompressor (default) and Closure Compiler\n\n"
790 
791                 + "JavaScript Compression Options for YUI Compressor:\n"
792                 + " --nomunge                     Minify only, do not obfuscate\n"
793                 + " --preserve-semi               Preserve all semicolons\n"
794                 + " --disable-optimizations       Disable all micro optimizations\n"
795                 + " --line-break <column num>     Insert a line break after the specified column\n\n"
796 
797                 + "JavaScript Compression Options for Google Closure Compiler:\n"
798                 + " --closure-opt-level <simple|advanced|whitespace>\n"
799                 + "                               Sets level of optimization (simple by default)\n"
800                 + " --closure-externs <file>      Sets custom externs file, repeat for each file\n"
801                 + " --closure-custom-externs-only Disable default built-in externs\n\n"
802 
803                 + "CSS Compression Options for YUI Compressor:\n"
804                 + " --line-break <column num>     Insert a line break after the specified column\n\n"
805 
806                 + "Custom Block Preservation Options:\n" + " --preserve-php                Preserve <?php ... ?> tags\n"
807                 + " --preserve-server-script      Preserve <% ... %> tags\n"
808                 + " --preserve-ssi                Preserve <!--# ... --> tags\n"
809                 + " -p, --preserve <path>         Read regular expressions that define\n"
810                 + "                               custom preservation rules from a file\n\n"
811 
812                 + "Please note that if you enable CSS or JavaScript compression, additional\n"
813                 + "YUI Compressor or Google Closure Compiler jar files must be present\n"
814                 + "in the same directory as this jar."
815 
816         );
817     }
818 
819     /**
820      * The Class CompressorFileFilter.
821      */
822     private class CompressorFileFilter implements FileFilter {
823 
824         /** The filemask pattern. */
825         private Pattern filemaskPattern;
826 
827         /** The with dirs. */
828         private boolean withDirs;
829 
830         /**
831          * Instantiates a new compressor file filter.
832          *
833          * @param type
834          *            the type
835          * @param filemask
836          *            the filemask
837          * @param withDirs
838          *            the with dirs
839          */
840         public CompressorFileFilter(String type, String filemask, boolean withDirs) {
841 
842             this.withDirs = withDirs;
843 
844             if (filemask == null) {
845                 if (type != null && "xml".equalsIgnoreCase(type)) {
846                     filemaskPattern = Pattern.compile("^.*\\.xml$", Pattern.CASE_INSENSITIVE);
847                 } else {
848                     filemaskPattern = Pattern.compile("^.*\\.html?$", Pattern.CASE_INSENSITIVE);
849                 }
850             } else {
851                 // turn mask into regexp
852                 filemask = filemask.replaceAll(escRegEx("."), Matcher.quoteReplacement("\\."));
853                 filemask = filemask.replaceAll(escRegEx("*"), Matcher.quoteReplacement(".*"));
854                 filemask = filemask.replaceAll(escRegEx("?"), Matcher.quoteReplacement("."));
855                 filemask = filemask.replaceAll(escRegEx(";"), Matcher.quoteReplacement("$|^"));
856                 filemask = "^" + filemask + "$";
857 
858                 filemaskPattern = Pattern.compile(filemask, Pattern.CASE_INSENSITIVE);
859             }
860         }
861 
862         @Override
863         public boolean accept(File file) {
864             if (!withDirs) {
865                 // take only matching non-dirs
866                 if (!file.isDirectory()) {
867                     return filemaskPattern.matcher(file.getName()).matches();
868                 }
869             } else {
870                 // take matching files and dirs
871                 return file.isDirectory() || filemaskPattern.matcher(file.getName()).matches();
872             }
873             return false;
874         }
875 
876     }
877 
878 }