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