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