View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   */
19  package org.apache.rat;
20  
21  import java.io.File;
22  import java.io.FilenameFilter;
23  import java.io.IOException;
24  import java.io.PrintStream;
25  import java.io.Serializable;
26  import java.nio.charset.StandardCharsets;
27  import java.nio.file.Files;
28  import java.nio.file.Paths;
29  import java.util.Arrays;
30  import java.util.Comparator;
31  import java.util.List;
32  import java.util.regex.PatternSyntaxException;
33  import java.util.stream.Collectors;
34  
35  import org.apache.commons.cli.CommandLine;
36  import org.apache.commons.cli.DefaultParser;
37  import org.apache.commons.cli.HelpFormatter;
38  import org.apache.commons.cli.Option;
39  import org.apache.commons.cli.OptionGroup;
40  import org.apache.commons.cli.Options;
41  import org.apache.commons.cli.ParseException;
42  import org.apache.commons.io.FileUtils;
43  import org.apache.commons.io.filefilter.NameFileFilter;
44  import org.apache.commons.io.filefilter.NotFileFilter;
45  import org.apache.commons.io.filefilter.OrFileFilter;
46  import org.apache.commons.io.filefilter.RegexFileFilter;
47  import org.apache.commons.io.filefilter.WildcardFileFilter;
48  import org.apache.commons.lang3.StringUtils;
49  import org.apache.rat.config.AddLicenseHeaders;
50  import org.apache.rat.license.LicenseSetFactory.LicenseFilter;
51  import org.apache.rat.report.IReportable;
52  import org.apache.rat.utils.DefaultLog;
53  import org.apache.rat.utils.Log;
54  import org.apache.rat.utils.Log.Level;
55  import org.apache.rat.walker.ArchiveWalker;
56  import org.apache.rat.walker.DirectoryWalker;
57  
58  /**
59   * The CLI based configuration object for report generation.
60   */
61  public class Report {
62      /**
63       * Adds license headers to files missing headers.
64       */
65      private static final String ADD = "A";
66      private static final String ADD_OLD = "a";
67      /**
68       * Forces changes to be written to new files.
69       */
70      private static final String FORCE = "f";
71      /**
72       * Defines the copyright header to add to the file.
73       */
74      private static final String COPYRIGHT = "c";
75      /**
76       * Name of File to exclude from report consideration.
77       */
78      private static final String EXCLUDE_CLI = "e";
79      /**
80       * Name of file that contains a list of files to exclude from report
81       * consideration.
82       */
83      private static final String EXCLUDE_FILE_CLI = "E";
84      /**
85       * The stylesheet to use to style the XML output.
86       */
87      private static final String STYLESHEET_CLI = "s";
88      /**
89       * Produce help
90       */
91      private static final String HELP = "h";
92      /**
93       * Flag to identify a file with license definitions.
94       */
95      private static final String LICENSES = "licenses";
96      /**
97       * Do not use the default files.
98       */
99      private static final String NO_DEFAULTS = "no-default-licenses";
100     /**
101      * Scan hidden directories.
102      */
103     private static final String SCAN_HIDDEN_DIRECTORIES = "scan-hidden-directories";
104     /**
105      * List the licenses that were used for the run.
106      */
107     private static final String LIST_LICENSES = "list-licenses";
108 
109     /**
110      * List the all families for the run.
111      */
112     private static final String LIST_FAMILIES = "list-families";
113 
114     private static final String LOG_LEVEL = "log-level";
115 
116     /**
117      * Set unstyled XML output
118      */
119     private static final String XML = "x";
120 
121 
122     /**
123      * Processes the command line and builds a configuration and executes the
124      * report.
125      * 
126      * @param args the arguments.
127      * @throws Exception on error.
128      */
129     public static void main(String[] args) throws Exception {
130         Options opts = buildOptions();
131         CommandLine cl;
132         try {
133             cl = new DefaultParser().parse(opts, args);
134         } catch (ParseException e) {
135             DefaultLog.INSTANCE.error(e.getMessage());
136             DefaultLog.INSTANCE.error("Please use the \"--help\" option to see a list of valid commands and options");
137             System.exit(1);
138             return; // dummy return (won't be reached) to avoid Eclipse complaint about possible NPE
139                     // for "cl"
140         }
141 
142         if (cl.hasOption(LOG_LEVEL)) {
143             try {
144                 Log.Level level = Log.Level.valueOf(cl.getOptionValue(LOG_LEVEL).toUpperCase());
145                 DefaultLog.INSTANCE.setLevel(level);
146             } catch (IllegalArgumentException e) {
147                 DefaultLog.INSTANCE.warn(String.format("Invalid Log Level (%s) specified.", cl.getOptionValue(LOG_LEVEL)));
148                 DefaultLog.INSTANCE.warn(String.format("Log level set at: %s", DefaultLog.INSTANCE.getLevel()));
149             }
150         }
151         if (cl.hasOption(HELP)) {
152             printUsage(opts);
153         }
154 
155         args = cl.getArgs();
156         if (args == null || args.length != 1) {
157             printUsage(opts);
158         } else {
159             ReportConfiguration configuration = createConfiguration(args[0], cl);
160             configuration.validate(DefaultLog.INSTANCE::error);
161 
162             boolean dryRun = false;
163             
164             if (cl.hasOption(LIST_FAMILIES)) {
165                 LicenseFilter f = LicenseFilter.fromText(cl.getOptionValue(LIST_FAMILIES));
166                 if (f != LicenseFilter.none) {                        
167                     dryRun = true;
168                     Reporter.listLicenseFamilies(configuration, f);
169                 }
170             }
171             if (cl.hasOption(LIST_LICENSES)) {
172                 LicenseFilter f = LicenseFilter.fromText(cl.getOptionValue(LIST_LICENSES));
173                 if (f != LicenseFilter.none) {                        
174                     dryRun = true;
175                     Reporter.listLicenses(configuration, f);
176                 }
177             }
178             
179             if (!dryRun) {
180                 Reporter.report(configuration);
181             }
182         }
183     }
184 
185     static ReportConfiguration createConfiguration(String baseDirectory, CommandLine cl) throws IOException {
186         final ReportConfiguration configuration = new ReportConfiguration(DefaultLog.INSTANCE);
187 
188         if (cl.hasOption('o')) {
189             configuration.setOut(new File(cl.getOptionValue('o')));
190         }
191 
192         if (cl.hasOption(SCAN_HIDDEN_DIRECTORIES)) {
193             configuration.setDirectoryFilter(null);
194         }
195 
196         if (cl.hasOption('a') || cl.hasOption('A')) {
197             configuration.setAddLicenseHeaders(cl.hasOption('f') ? AddLicenseHeaders.FORCED : AddLicenseHeaders.TRUE);
198             configuration.setCopyrightMessage(cl.getOptionValue("c"));
199         }
200 
201         if (cl.hasOption(EXCLUDE_CLI)) {
202             String[] excludes = cl.getOptionValues(EXCLUDE_CLI);
203             if (excludes != null) {
204                 final FilenameFilter filter = parseExclusions(Arrays.asList(excludes));
205                 configuration.setInputFileFilter(filter);
206             }
207         } else if (cl.hasOption(EXCLUDE_FILE_CLI)) {
208             String excludeFileName = cl.getOptionValue(EXCLUDE_FILE_CLI);
209             if (excludeFileName != null) {
210                 final FilenameFilter filter = parseExclusions(
211                         FileUtils.readLines(new File(excludeFileName), StandardCharsets.UTF_8));
212                 configuration.setInputFileFilter(filter);
213             }
214         }
215 
216         if (cl.hasOption(XML)) {
217             configuration.setStyleReport(false);
218         } else {
219             configuration.setStyleReport(true);
220             if (cl.hasOption(STYLESHEET_CLI)) {
221                 String[] style = cl.getOptionValues(STYLESHEET_CLI);
222                 if (style.length != 1) {
223                     DefaultLog.INSTANCE.error("Please specify a single stylesheet");
224                     System.exit(1);
225                 }
226                 configuration.setStyleSheet(() -> Files.newInputStream(Paths.get(style[0])));
227             }
228         }
229 
230         Defaults.Builder defaultBuilder = Defaults.builder();
231         if (cl.hasOption(NO_DEFAULTS)) {
232             defaultBuilder.noDefault();
233         }
234         if (cl.hasOption(LICENSES)) {
235             for (String fn : cl.getOptionValues(LICENSES)) {
236                 defaultBuilder.add(fn);
237             }
238         }
239         Defaults defaults = defaultBuilder.build();
240         configuration.setFrom(defaults);
241         configuration.setReportable(getDirectory(baseDirectory, configuration));
242         return configuration;
243     }
244     
245     /**
246      * Creates a filename filter from patterns to exclude.
247      * 
248      * @param excludes the list of patterns to exclude.
249      * @return the FilenameFilter tht excludes the patterns
250      */
251     static FilenameFilter parseExclusions(List<String> excludes) {
252         final OrFileFilter orFilter = new OrFileFilter();
253         int ignoredLines = 0;
254         for (String exclude : excludes) {
255             try {
256                 // skip comments
257                 if (exclude.startsWith("#") || StringUtils.isEmpty(exclude)) {
258                     ignoredLines++;
259                     continue;
260                 }
261 
262                 String exclusion = exclude.trim();
263                 // interpret given patterns as regular expression, direct file names or
264                 // wildcards to give users more choices to configure exclusions
265                 orFilter.addFileFilter(new RegexFileFilter(exclusion));
266                 orFilter.addFileFilter(new NameFileFilter(exclusion));
267                 orFilter.addFileFilter(WildcardFileFilter.builder().setWildcards(exclusion).get());
268             } catch (PatternSyntaxException e) {
269                 DefaultLog.INSTANCE.error("Will skip given exclusion '" + exclude + "' due to " + e);
270             }
271         }
272         DefaultLog.INSTANCE.error("Ignored " + ignoredLines + " lines in your exclusion files as comments or empty lines.");
273         return new NotFileFilter(orFilter);
274     }
275 
276     static Options buildOptions() {
277         String licFilterValues = String.join(", ", 
278                 Arrays.stream(LicenseFilter.values()).map(LicenseFilter::name).collect(Collectors.toList()));
279 
280         Options opts = new Options()
281         
282         .addOption(
283                 Option.builder().hasArg(true).longOpt(LIST_FAMILIES)
284                 .desc("List the defined license families (default is none). Valid options are: "+licFilterValues+".")
285                 .build())
286         .addOption(
287                 Option.builder().hasArg(true).longOpt(LIST_LICENSES)
288                 .desc("List the defined licenses (default is none). Valid options are: "+licFilterValues+".")
289                 .build())
290 
291         .addOption(new Option(HELP, "help", false, "Print help for the RAT command line interface and exit."));
292         
293 
294         Option out = new Option("o", "out", true,
295                 "Define the output file where to write a report to (default is System.out).");
296         opts.addOption(out);
297 
298         String defaultHandlingText = " By default all approved default licenses are used";
299         Option noDefaults = new Option(null, NO_DEFAULTS, false, "Ignore default configuration." + defaultHandlingText);
300         opts.addOption(noDefaults);
301 
302         opts.addOption(null, LICENSES, true, "File names or URLs for license definitions");
303         opts.addOption(null, SCAN_HIDDEN_DIRECTORIES, false, "Scan hidden directories");
304 
305         OptionGroup addLicenseGroup = new OptionGroup();
306         String addLicenseDesc = "Add the default license header to any file with an unknown license that is not in the exclusion list. "
307                 + "By default new files will be created with the license header, "
308                 + "to force the modification of existing files use the --force option.";
309 
310         // RAT-85/RAT-203: Deprecated! added only for convenience and for backwards
311         // compatibility
312         Option addLicence = new Option(ADD_OLD, "addLicence", false, addLicenseDesc);
313         addLicenseGroup.addOption(addLicence);
314         Option addLicense = new Option(ADD, "addLicense", false, addLicenseDesc);
315         addLicenseGroup.addOption(addLicense);
316         opts.addOptionGroup(addLicenseGroup);
317 
318         Option write = new Option(FORCE, "force", false,
319                 "Forces any changes in files to be written directly to the source files (i.e. new files are not created).");
320         opts.addOption(write);
321 
322         Option copyright = new Option(COPYRIGHT, "copyright", true,
323                 "The copyright message to use in the license headers, usually in the form of \"Copyright 2008 Foo\"");
324         opts.addOption(copyright);
325 
326         final Option exclude = Option.builder(EXCLUDE_CLI).argName("expression").longOpt("exclude").hasArgs()
327                 .desc("Excludes files matching wildcard <expression>. "
328                         + "Note that --dir is required when using this parameter. " + "Allows multiple arguments.")
329                 .build();
330         opts.addOption(exclude);
331 
332         final Option excludeFile = Option.builder(EXCLUDE_FILE_CLI).argName("fileName").longOpt("exclude-file")
333                 .hasArgs().desc("Excludes files matching regular expression in <file> "
334                         + "Note that --dir is required when using this parameter. ")
335                 .build();
336         opts.addOption(excludeFile);
337 
338         Option dir = new Option("d", "dir", false, "Used to indicate source when using --exclude");
339         opts.addOption(dir);
340 
341         opts.addOption( Option.builder().argName("level").longOpt(LOG_LEVEL)
342                 .hasArgs().desc("sets the log level.  Valid options are: DEBUG, INFO, WARN, ERROR, OFF")
343                 .build() );
344         
345         OptionGroup outputType = new OptionGroup();
346 
347         Option xml = new Option(XML, "xml", false, "Output the report in raw XML format.  Not compatible with -s");
348         outputType.addOption(xml);
349 
350         Option xslt = new Option(STYLESHEET_CLI, "stylesheet", true,
351                 "XSLT stylesheet to use when creating the" + " report.  Not compatible with -x");
352         outputType.addOption(xslt);
353         opts.addOptionGroup(outputType);
354         
355         
356 
357         return opts;
358     }
359 
360     private static void printUsage(Options opts) {
361         HelpFormatter f = new HelpFormatter();
362         f.setOptionComparator(new OptionComparator());
363         String header = "\nAvailable options";
364 
365         String footer = "\nNOTE:\n" + "Rat is really little more than a grep ATM\n"
366                 + "Rat is also rather memory hungry ATM\n" + "Rat is very basic ATM\n"
367                 + "Rat highlights possible issues\n" + "Rat reports require interpretation\n"
368                 + "Rat often requires some tuning before it runs well against a project\n"
369                 + "Rat relies on heuristics: it may miss issues\n";
370 
371         f.printHelp("java -jar apache-rat/target/apache-rat-CURRENT-VERSION.jar [options] [DIR|TARBALL]", header, opts,
372                 footer, false);
373         System.exit(0);
374     }
375 
376     private Report() {
377         // do not instantiate
378     }
379 
380     /**
381      * Creates an IReportable object from the directory name and ReportConfiguration
382      * object.
383      * 
384      * @param baseDirectory the directory that contains the files to report on.
385      * @param config the ReportConfiguration.
386      * @return the IReportable instance containing the files.
387      */
388     private static IReportable getDirectory(String baseDirectory, ReportConfiguration config) {
389         try (PrintStream out = new PrintStream(config.getOutput().get())) {
390             File base = new File(baseDirectory);
391             
392             if (!base.exists()) {
393                 config.getLog().log(Level.ERROR, "Directory '"+baseDirectory+"' does not exist");
394                 return null;
395             }
396 
397             if (base.isDirectory()) {
398                 return new DirectoryWalker(base, config.getInputFileFilter(), config.getDirectoryFilter());
399             }
400 
401             try {
402                 return new ArchiveWalker(base, config.getInputFileFilter());
403             } catch (IOException ex) {
404                 config.getLog().log(Level.ERROR, "file '"+baseDirectory+"' is not valid gzip data.");
405                 return null;
406             }
407         } catch (IOException e) {
408             throw new ConfigurationException("Error opening output", e);
409         }
410     }
411 
412     /**
413      * This class implements the {@code Comparator} interface for comparing Options.
414      */
415     private static class OptionComparator implements Comparator<Option>, Serializable {
416         /** The serial version UID. */
417         private static final long serialVersionUID = 5305467873966684014L;
418 
419         private String getKey(Option opt) {
420             String key = opt.getOpt();
421             key = key == null ? opt.getLongOpt() : key;
422             return key;
423         }
424 
425         /**
426          * Compares its two arguments for order. Returns a negative integer, zero, or a
427          * positive integer as the first argument is less than, equal to, or greater
428          * than the second.
429          *
430          * @param opt1 The first Option to be compared.
431          * @param opt2 The second Option to be compared.
432          * @return a negative integer, zero, or a positive integer as the first argument
433          * is less than, equal to, or greater than the second.
434          */
435         @Override
436         public int compare(final Option opt1, final Option opt2) {
437             return getKey(opt1).compareToIgnoreCase(getKey(opt2));
438         }
439     }
440 }