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