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