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.IOException;
23  import java.io.PrintWriter;
24  import java.io.Serializable;
25  import java.nio.charset.StandardCharsets;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.Comparator;
29  import java.util.Map;
30  import java.util.Optional;
31  import java.util.TreeMap;
32  import java.util.function.Consumer;
33  import java.util.function.Supplier;
34  import java.util.stream.Collectors;
35  
36  import org.apache.commons.cli.CommandLine;
37  import org.apache.commons.cli.DefaultParser;
38  import org.apache.commons.cli.Option;
39  import org.apache.commons.cli.Options;
40  import org.apache.commons.cli.ParseException;
41  import org.apache.rat.api.Document;
42  import org.apache.rat.commandline.Arg;
43  import org.apache.rat.commandline.ArgumentContext;
44  import org.apache.rat.commandline.StyleSheets;
45  import org.apache.rat.config.exclusion.StandardCollection;
46  import org.apache.rat.document.DocumentName;
47  import org.apache.rat.document.DocumentNameMatcher;
48  import org.apache.rat.document.FileDocument;
49  import org.apache.rat.help.Licenses;
50  import org.apache.rat.license.LicenseSetFactory;
51  import org.apache.rat.report.IReportable;
52  import org.apache.rat.report.claim.ClaimStatistic;
53  import org.apache.rat.utils.DefaultLog;
54  import org.apache.rat.utils.Log.Level;
55  import org.apache.rat.walker.ArchiveWalker;
56  import org.apache.rat.walker.DirectoryWalker;
57  
58  import static java.lang.String.format;
59  
60  /**
61   * The collection of standard options for the CLI as well as utility methods to manage them and methods to create the
62   * ReportConfiguration from the options and an array of arguments.
63   */
64  public final class OptionCollection {
65  
66      private OptionCollection() {
67          // do not instantiate
68      }
69  
70      /**
71       * The Option comparator to sort the help.
72       */
73      public static final Comparator<Option> OPTION_COMPARATOR = new OptionComparator();
74  
75      /** The Help option */
76      public static final Option HELP = new Option("?", "help", false, "Print help for the RAT command line interface and exit.");
77  
78      /** A mapping of {@code argName(value)} values to a description of those values. */
79      @Deprecated
80      private static final Map<String, Supplier<String>> ARGUMENT_TYPES;
81      static {
82          ARGUMENT_TYPES = new TreeMap<>();
83          for (ArgumentType argType : ArgumentType.values()) {
84              ARGUMENT_TYPES.put(argType.getDisplayName(), argType.description);
85          }
86      }
87  
88      /**
89       * Gets the mapping of {@code argName(value)} values to a description of those values.
90       * @return the mapping of {@code argName(value)} values to a description of those values.
91       * @deprecated use {@link ArgumentType}
92       */
93      @Deprecated
94      public static Map<String, Supplier<String>> getArgumentTypes() {
95          return Collections.unmodifiableMap(ARGUMENT_TYPES);
96      }
97  
98      /**
99       * Join a collection of objects together as a comma separated list of their string values.
100      * @param args the objects to join together.
101      * @return the comma separated string.
102      */
103     private static String asString(final Object[] args) {
104         return Arrays.stream(args).map(Object::toString).collect(Collectors.joining(", "));
105     }
106 
107     /**
108      * Parses the standard options to create a ReportConfiguration.
109      *
110      * @param workingDirectory The directory to resolve relative file names against.
111      * @param args the arguments to parse
112      * @param helpCmd the help command to run when necessary.
113      * @return a ReportConfiguration or {@code null} if Help was printed.
114      * @throws IOException on error.
115      */
116     public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args, final Consumer<Options> helpCmd) throws IOException {
117         return parseCommands(workingDirectory, args, helpCmd, false);
118     }
119 
120     /**
121      * Parses the standard options to create a ReportConfiguration.
122      *
123      * @param workingDirectory The directory to resolve relative file names against.
124      * @param args the arguments to parse.
125      * @param helpCmd the help command to run when necessary.
126      * @param noArgs If {@code true} then the commands do not need extra arguments.
127      * @return a ReportConfiguration or {@code null} if Help was printed.
128      * @throws IOException on error.
129      */
130     public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args,
131                                                     final Consumer<Options> helpCmd, final boolean noArgs) throws IOException {
132 
133         Options opts = buildOptions();
134         CommandLine commandLine;
135         try {
136             commandLine = DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter())
137                     .setAllowPartialMatching(true).build().parse(opts, args);
138         } catch (ParseException e) {
139             DefaultLog.getInstance().error(e.getMessage());
140             DefaultLog.getInstance().error("Please use the \"--help\" option to see a list of valid commands and options.", e);
141             System.exit(1);
142             return null; // dummy return (won't be reached) to avoid Eclipse complaint about possible NPE
143             // for "commandLine"
144         }
145 
146         ArgumentContext argumentContext = new ArgumentContext(workingDirectory, commandLine);
147         Arg.processLogLevel(argumentContext, CLIOptionCollection.INSTANCE);
148 
149         if (commandLine.hasOption(HELP)) {
150             helpCmd.accept(opts);
151             return null;
152         }
153 
154         if (commandLine.hasOption(Arg.HELP_LICENSES.option())) {
155             new Licenses(createConfiguration(argumentContext), new PrintWriter(System.out, false, StandardCharsets.UTF_8)).printHelp();
156             return null;
157         }
158 
159         ReportConfiguration configuration = createConfiguration(argumentContext);
160         if (!noArgs && !configuration.hasSource()) {
161             String msg = "No directories or files specified for scanning. Did you forget to close a multi-argument option?";
162             DefaultLog.getInstance().error(msg);
163             helpCmd.accept(opts);
164             return null;
165         }
166 
167         return configuration;
168     }
169 
170     /**
171      * Create the report configuration.
172      * Note: this method is package private for testing.
173      * You probably want one of the {@code ParseCommands} methods.
174      * @param argumentContext The context to execute in.
175      * @return a ReportConfiguration
176      * @see #parseCommands(File, String[], Consumer)
177      * @see #parseCommands(File, String[], Consumer, boolean)
178      */
179     public static ReportConfiguration createConfiguration(final ArgumentContext argumentContext) {
180         argumentContext.processArgs(CLIOptionCollection.INSTANCE);
181         final ReportConfiguration configuration = argumentContext.getConfiguration();
182         final CommandLine commandLine = argumentContext.getCommandLine();
183         Optional<Option> dirOpt = CLIOptionCollection.INSTANCE.getSelected(Arg.DIR);
184         if (dirOpt.isPresent()) {
185             try {
186                 configuration.addSource(getReportable(commandLine.getParsedOptionValue(
187                         dirOpt.get()), configuration));
188             } catch (ParseException e) {
189                 throw new ConfigurationException("Unable to set parse " + dirOpt.get(), e);
190             }
191         }
192         for (String s : commandLine.getArgs()) {
193             IReportable reportable = getReportable(new File(s), configuration);
194             if (reportable != null) {
195                 configuration.addSource(reportable);
196             }
197         }
198         return configuration;
199     }
200 
201     /**
202      * Create an {@code Options} object from the list of defined Options.
203      * Mutually exclusive options must be listed in an OptionGroup.
204      * @return the Options comprised of the Options defined in this class.
205      */
206     public static Options buildOptions() {
207         return CLIOptionCollection.INSTANCE.getOptions();
208     }
209 
210     /**
211      * Creates an IReportable object from the directory name and ReportConfiguration
212      * object.
213      *
214      * @param base the directory that contains the files to report on.
215      * @param config the ReportConfiguration.
216      * @return the IReportable instance containing the files.
217      */
218     public static IReportable getReportable(final File base, final ReportConfiguration config) {
219         File absBase = base.getAbsoluteFile();
220         DocumentName documentName = DocumentName.builder(absBase).build();
221         if (!absBase.exists()) {
222             DefaultLog.getInstance().error("Directory '" + documentName + "' does not exist.");
223             return null;
224         }
225         DocumentNameMatcher documentExcluder = config.getDocumentExcluder(documentName);
226 
227         Document doc = new FileDocument(documentName, absBase, documentExcluder);
228         if (!documentExcluder.matches(doc.getName())) {
229             DefaultLog.getInstance().error("Directory '" + documentName + "' is in excluded list.");
230             return null;
231         }
232 
233         if (absBase.isDirectory()) {
234             return new DirectoryWalker(doc);
235         }
236 
237         return new ArchiveWalker(doc);
238     }
239 
240     /**
241      * This class implements the {@code Comparator} interface for comparing Options.
242      */
243     private static final class OptionComparator implements Comparator<Option>, Serializable {
244         /** The serial version UID.  */
245         private static final long serialVersionUID = 5305467873966684014L;
246 
247         private String getKey(final Option opt) {
248             String key = opt.getOpt();
249             key = key == null ? opt.getLongOpt() : key;
250             return key;
251         }
252 
253         /**
254          * Compares its two arguments for order. Returns a negative integer, zero, or a
255          * positive integer as the first argument is less than, equal to, or greater
256          * than the second.
257          *
258          * @param opt1 The first Option to be compared.
259          * @param opt2 The second Option to be compared.
260          * @return a negative integer, zero, or a positive integer as the first argument
261          * is less than, equal to, or greater than the second.
262          */
263         @Override
264         public int compare(final Option opt1, final Option opt2) {
265             return getKey(opt1).compareToIgnoreCase(getKey(opt2));
266         }
267     }
268 
269     public enum ArgumentType {
270         /**
271          * A plain file.
272          */
273         FILE("File", () -> "A file name."),
274         /**
275          * An Integer.
276          */
277         INTEGER("Integer", () -> "An integer value."),
278         /**
279          * A directory or archive.
280          */
281         DIRORARCHIVE("DirOrArchive", () -> "A directory or archive file to scan."),
282         /**
283          * A matching expression.
284          */
285         EXPRESSION("Expression", () -> "A file matching pattern usually of the form used in Ant build files and " +
286                 "'.gitignore' files (see https://ant.apache.org/manual/dirtasks.html#patterns for examples). " +
287                 "Regular expression patterns may be specified by surrounding the pattern with '%regex[' and ']'. " +
288                 "For example '%regex[[A-Z].*]' would match files and directories that start with uppercase latin letters."),
289         /**
290          * A license filter.
291          */
292         LICENSEFILTER("LicenseFilter", () -> format("A defined filter for the licenses to include. Valid values: %s.",
293                 asString(LicenseSetFactory.LicenseFilter.values()))),
294         /**
295          * A log level.
296          */
297         LOGLEVEL("LogLevel", () -> format("The log level to use. Valid values %s.", asString(Level.values()))),
298         /**
299          * A processing type.
300          */
301         PROCESSINGTYPE("ProcessingType", () -> format("Specifies how to process file types. Valid values are: %s%n",
302                 Arrays.stream(ReportConfiguration.Processing.values())
303                         .map(v -> format("\t%s: %s", v.name(), v.desc()))
304                         .collect(Collectors.joining(System.lineSeparator())))),
305         /**
306          * A style sheet.
307          */
308         STYLESHEET("StyleSheet", () -> format("Either an external xsl file or one of the internal named sheets. Internal sheets are: %n%s",
309                 Arrays.stream(StyleSheets.values())
310                         .map(v -> format("\t%s: %s%n", v.arg(), v.desc()))
311                         .collect(Collectors.joining(System.lineSeparator())))),
312         /**
313          * A license id.
314          */
315         LICENSEID("LicenseID", () -> "The ID for a license."),
316         /**
317          * A license family id.
318          */
319         FAMILYID("FamilyID", () -> "The ID for a license family."),
320         /**
321          * A standard collection name.
322          */
323         STANDARDCOLLECTION("StandardCollection", () -> format("Defines standard expression patterns (see above). Valid values are: %n%s%n",
324                 Arrays.stream(StandardCollection.values())
325                         .map(v -> format("\t%s: %s%n", v.name(), v.desc()))
326                         .collect(Collectors.joining(System.lineSeparator())))),
327         /**
328          * A Counter pattern name
329          */
330         COUNTERPATTERN("CounterPattern", () -> format("A pattern comprising one of the following prefixes followed by " +
331                         "a colon and a count (e.g. %s:5).  Prefixes are %n%s.", ClaimStatistic.Counter.UNAPPROVED,
332                 Arrays.stream(ClaimStatistic.Counter.values())
333                         .map(v -> format("\t%s: %s Default range [%s, %s]%n", v.name(), v.getDescription(),
334                                 v.getDefaultMinValue(),
335                                 v.getDefaultMaxValue() == -1 ? "unlimited" : v.getDefaultMaxValue()))
336                         .collect(Collectors.joining(System.lineSeparator())))),
337         /**
338          * A generic argument.
339          */
340         ARG("Arg", () -> "A string"),
341         /**
342          * No argument.
343          */
344         NONE("", () -> "");
345 
346         /**
347          * The display name
348          */
349         private final String displayName;
350         /**
351          * A supplier of the description
352          */
353         private final Supplier<String> description;
354 
355         ArgumentType(final String name,
356                      final Supplier<String> description) {
357             this.displayName = name;
358             this.description = description;
359         }
360 
361         /**
362          * Gets the display name.
363          * @return the display name.
364          */
365         public String getDisplayName() {
366             return displayName;
367         }
368 
369         /**
370          * Gets the description.
371          * @return the description.
372          */
373         public Supplier<String> description() {
374             return description;
375         }
376 
377         /**
378          * Get the matching Argument type.
379          * @param displayName the display name for the desired type.
380          * @return An optional with the ArgumentType or an empty optional if none exists.
381          */
382         public static Optional<ArgumentType> forDisplayName(final String displayName) {
383             return Arrays.stream(ArgumentType.values()).filter(argType -> argType.displayName.equals(displayName))
384                     .findAny();
385         }
386     }
387 }