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.commandline;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.lang.reflect.Array;
26  import java.nio.charset.StandardCharsets;
27  import java.nio.file.Files;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.function.Predicate;
34  
35  import org.apache.commons.cli.AlreadySelectedException;
36  import org.apache.commons.cli.CommandLine;
37  import org.apache.commons.cli.DeprecatedAttributes;
38  import org.apache.commons.cli.Option;
39  import org.apache.commons.cli.OptionGroup;
40  import org.apache.commons.cli.Options;
41  import org.apache.commons.cli.ParseException;
42  import org.apache.commons.io.IOUtils;
43  import org.apache.commons.io.function.IOSupplier;
44  import org.apache.commons.lang3.tuple.Pair;
45  import org.apache.rat.ConfigurationException;
46  import org.apache.rat.Defaults;
47  import org.apache.rat.ReportConfiguration;
48  import org.apache.rat.config.AddLicenseHeaders;
49  import org.apache.rat.config.exclusion.ExclusionUtils;
50  import org.apache.rat.config.exclusion.StandardCollection;
51  import org.apache.rat.document.DocumentName;
52  import org.apache.rat.document.DocumentNameMatcher;
53  import org.apache.rat.license.LicenseSetFactory;
54  import org.apache.rat.report.claim.ClaimStatistic.Counter;
55  import org.apache.rat.utils.DefaultLog;
56  import org.apache.rat.utils.Log;
57  
58  import static java.lang.String.format;
59  
60  /**
61   * An enumeration of options.
62   * <p>
63   * Each Arg contains an OptionGroup that contains the individual options that all resolve to the same option.
64   * This allows us to deprecate options as we move forward in development.
65   */
66  public enum Arg {
67  
68      ///////////////////////// EDIT OPTIONS
69      /**
70       * Defines options to add copyright to files
71       */
72      EDIT_COPYRIGHT(new OptionGroup()
73              .addOption(Option.builder("c")
74                      .longOpt("copyright").hasArg()
75                      .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
76                              .setDescription(StdMsgs.useMsg("--edit-copyright")).get())
77                      .desc("The copyright message to use in the license headers.")
78                      .build())
79              .addOption(Option.builder().longOpt("edit-copyright").hasArg()
80                      .desc("The copyright message to use in the license headers. Usually in the form of \"Copyright 2008 Foo\".  "
81                              + "Only valid with --edit-license")
82                      .build())),
83  
84      /**
85       * Causes file updates to overwrite existing files.
86       */
87      EDIT_OVERWRITE(new OptionGroup()
88              .addOption(Option.builder("f").longOpt("force")
89                      .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
90                              .setDescription(StdMsgs.useMsg("--edit-overwrite")).get())
91                      .desc("Forces any changes in files to be written directly to the source files so that new files are not created.")
92                      .build())
93              .addOption(Option.builder().longOpt("edit-overwrite")
94                      .desc("Forces any changes in files to be written directly to the source files so that new files are not created. "
95                              + "Only valid with --edit-license.")
96                      .build())),
97  
98      /**
99       * Defines options to add licenses to files
100      */
101     EDIT_ADD(new OptionGroup()
102             .addOption(Option.builder("a")
103                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
104                             .setDescription(StdMsgs.useMsg("--edit-license")).get())
105                     .build())
106             .addOption(Option.builder("A").longOpt("addLicense")
107                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
108                             .setDescription(StdMsgs.useMsg("--edit-license")).get())
109                     .desc("Add the default license header to any file with an unknown license that is not in the exclusion list.")
110                     .build())
111             .addOption(Option.builder().longOpt("edit-license").desc(
112                     "Add the default license header to any file with an unknown license that is not in the exclusion list. "
113                             + "By default new files will be created with the license header, "
114                             + "to force the modification of existing files use the --edit-overwrite option.").build()
115             )),
116 
117     //////////////////////////// CONFIGURATION OPTIONS
118     /**
119      * Group of options that read a configuration file
120      */
121     CONFIGURATION(new OptionGroup()
122             .addOption(Option.builder().longOpt("config").hasArgs().argName("File")
123                     .desc("File names for system configuration.")
124                     .converter(Converters.FILE_CONVERTER)
125                     .type(File.class)
126                     .build())
127             .addOption(Option.builder().longOpt("licenses").hasArgs().argName("File")
128                     .desc("File names for system configuration.")
129                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--config")).get())
130                     .converter(Converters.FILE_CONVERTER)
131                     .type(File.class)
132                     .build())),
133 
134     /**
135      * Group of options that skip the default configuration file
136      */
137     CONFIGURATION_NO_DEFAULTS(new OptionGroup()
138             .addOption(Option.builder().longOpt("configuration-no-defaults")
139                     .desc("Ignore default configuration.").build())
140             .addOption(Option.builder().longOpt("no-default-licenses")
141                     .deprecated(DeprecatedAttributes.builder()
142                             .setSince("0.17")
143                             .setForRemoval(true)
144                             .setDescription(StdMsgs.useMsg("--configuration-no-defaults")).get())
145                     .desc("Ignore default configuration.")
146                     .build())),
147 
148     /**
149      * Option that adds approved licenses to the list
150      */
151     LICENSES_APPROVED(new OptionGroup().addOption(Option.builder().longOpt("licenses-approved").hasArg().argName("LicenseID")
152             .desc("A comma separated list of approved License IDs. These licenses will be added to the list of approved licenses.")
153                     .converter(Converters.TEXT_LIST_CONVERTER)
154                     .type(String[].class)
155             .build())),
156 
157     /**
158      * Option that adds approved licenses from a file
159      */
160     LICENSES_APPROVED_FILE(new OptionGroup().addOption(Option.builder().longOpt("licenses-approved-file").hasArg().argName("File")
161             .desc("Name of file containing comma separated lists of approved License IDs.")
162             .converter(Converters.FILE_CONVERTER)
163             .type(File.class)
164             .build())),
165 
166     /**
167      * Option that specifies approved license families
168      */
169     FAMILIES_APPROVED(new OptionGroup().addOption(Option.builder().longOpt("license-families-approved").hasArg().argName("FamilyID")
170             .desc("A comma separated list of approved license family IDs. These license families will be added to the list of approved license families.")
171             .converter(Converters.TEXT_LIST_CONVERTER)
172             .type(String[].class)
173             .build())),
174 
175     /**
176      * Option that specifies approved license families from a file
177      */
178     FAMILIES_APPROVED_FILE(new OptionGroup().addOption(Option.builder().longOpt("license-families-approved-file").hasArg().argName("File")
179             .desc("Name of file containing comma separated lists of approved family IDs.")
180             .converter(Converters.FILE_CONVERTER)
181             .type(File.class)
182             .build())),
183 
184     /**
185      * Option to remove licenses from the approved list
186      */
187     LICENSES_DENIED(new OptionGroup().addOption(Option.builder().longOpt("licenses-denied").hasArg().argName("LicenseID")
188             .desc("A comma separated list of denied License IDs. " +
189                     "These licenses will be removed from the list of approved licenses. " +
190                     "Once licenses are removed they can not be added back.")
191             .converter(Converters.TEXT_LIST_CONVERTER)
192             .type(String[].class)
193             .build())),
194 
195     /**
196      * Option to read a file licenses to be removed from the approved list.
197      */
198     LICENSES_DENIED_FILE(new OptionGroup().addOption(Option.builder().longOpt("licenses-denied-file")
199             .hasArg().argName("File").type(File.class)
200             .converter(Converters.FILE_CONVERTER)
201             .desc("Name of file containing comma separated lists of the denied license IDs. " +
202                     "These licenses will be removed from the list of approved licenses. " +
203                     "Once licenses are removed they can not be added back.")
204             .build())),
205 
206     /**
207      * Option to list license families to remove from the approved list.
208      */
209     FAMILIES_DENIED(new OptionGroup().addOption(Option.builder().longOpt("license-families-denied")
210             .hasArg().argName("FamilyID")
211             .desc("A comma separated list of denied License family IDs. " +
212                     "These license families will be removed from the list of approved licenses. " +
213                     "Once license families are removed they can not be added back.")
214             .converter(Converters.TEXT_LIST_CONVERTER)
215             .type(String[].class)
216             .build())),
217 
218     /**
219      * Option to read a list of license families to remove from the approved list.
220      */
221     FAMILIES_DENIED_FILE(new OptionGroup().addOption(Option.builder().longOpt("license-families-denied-file").hasArg().argName("File")
222             .desc("Name of file containing comma separated lists of denied license IDs. " +
223                     "These license families will be removed from the list of approved licenses. " +
224                     "Once license families are removed they can not be added back.")
225             .type(File.class)
226             .converter(Converters.FILE_CONVERTER)
227             .build())),
228 
229     /**
230      * Option to specify an acceptable number of various counters.
231      */
232     COUNTER_MAX(new OptionGroup().addOption(Option.builder().longOpt("counter-max").hasArgs().argName("CounterPattern")
233             .desc("The acceptable maximum number for the specified counter. A value of '-1' specifies an unlimited number.")
234             .converter(Converters.COUNTER_CONVERTER)
235             .type(Pair.class)
236             .build())),
237 
238     /**
239      * Option to specify an acceptable number of various counters.
240      */
241     COUNTER_MIN(new OptionGroup().addOption(Option.builder().longOpt("counter-min").hasArgs().argName("CounterPattern")
242             .desc("The minimum number for the specified counter.")
243             .converter(Converters.COUNTER_CONVERTER)
244             .type(Pair.class)
245             .build())),
246 
247 ////////////////// INPUT OPTIONS
248     /**
249      * Reads files to test from file.
250      */
251     SOURCE(new OptionGroup()
252             .addOption(Option.builder().longOpt("input-source").hasArgs().argName("File")
253                     .desc("A file containing file names to process. " +
254                             "File names must use linux directory separator ('/') or none at all. " +
255                             "File names that do not start with '/' are relative to the directory where the " +
256                             "argument is located.")
257                     .converter(Converters.FILE_CONVERTER)
258                     .type(File.class)
259                     .build())),
260 
261     /**
262      * Excludes files by expression
263      */
264     EXCLUDE(new OptionGroup()
265             .addOption(Option.builder("e").longOpt("exclude").hasArgs().argName("Expression")
266                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
267                             .setDescription(StdMsgs.useMsg("--input-exclude")).get())
268                     .desc("Excludes files matching <Expression>.")
269                     .build())
270             .addOption(Option.builder().longOpt("input-exclude").hasArgs().argName("Expression")
271                     .desc("Excludes files matching <Expression>.")
272                     .build())),
273 
274     /**
275      * Excludes files based on contents of a file.
276      */
277     EXCLUDE_FILE(new OptionGroup()
278             .addOption(Option.builder("E").longOpt("exclude-file")
279                     .argName("File").hasArg().type(File.class)
280                     .converter(Converters.FILE_CONVERTER)
281                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
282                             .setDescription(StdMsgs.useMsg("--input-exclude-file")).get())
283                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
284                     .build())
285             .addOption(Option.builder().longOpt("input-exclude-file")
286                     .argName("File").hasArg().type(File.class)
287                     .converter(Converters.FILE_CONVERTER)
288                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
289                     .build())),
290     /**
291      * Excludes files based on standard groupings.
292      */
293     EXCLUDE_STD(new OptionGroup()
294             .addOption(Option.builder().longOpt("input-exclude-std").argName("StandardCollection")
295                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
296                     .desc("Excludes files defined in standard collections based on commonly occurring groups.")
297                     .type(StandardCollection.class)
298                     .build())
299     ),
300 
301     /**
302      * Excludes files if they are smaller than the given threshold.
303      */
304     EXCLUDE_SIZE(new OptionGroup()
305             .addOption(Option.builder().longOpt("input-exclude-size").argName("Integer")
306                     .hasArg().type(Integer.class)
307                     .desc("Excludes files with sizes less than the given argument.")
308                     .build())
309     ),
310     /**
311      * Excludes files by expression.
312      */
313     INCLUDE(new OptionGroup()
314             .addOption(Option.builder().longOpt("input-include").hasArgs().argName("Expression")
315                     .desc("Includes files matching <Expression>. Will override excluded files.")
316                     .build())
317             .addOption(Option.builder().longOpt("include").hasArgs().argName("Expression")
318                     .desc("Includes files matching <Expression>. Will override excluded files.")
319                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
320                             .setDescription(StdMsgs.useMsg("--input-include")).get())
321                     .build())
322     ),
323 
324     /**
325      * Includes files based on contents of a file.
326      */
327     INCLUDE_FILE(new OptionGroup()
328             .addOption(Option.builder().longOpt("input-include-file")
329                     .argName("File").hasArg().type(File.class)
330                     .converter(Converters.FILE_CONVERTER)
331                     .desc("Reads <Expression> entries from a file. Entries will override excluded files.")
332                     .build())
333             .addOption(Option.builder().longOpt("includes-file")
334                     .argName("File").hasArg().type(File.class)
335                     .converter(Converters.FILE_CONVERTER)
336                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
337                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
338                             .setDescription(StdMsgs.useMsg("--input-include-file")).get())
339                     .build())),
340 
341     /**
342      * Includes files based on standard groups.
343      */
344     INCLUDE_STD(new OptionGroup()
345             .addOption(Option.builder().longOpt("input-include-std").argName("StandardCollection")
346                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
347                     .desc("Includes files defined in standard collections based on commonly occurring groups. " +
348                             "Will override excluded files.")
349                     .type(StandardCollection.class)
350                     .build())
351             .addOption(Option.builder().longOpt("scan-hidden-directories")
352                     .desc("Scans hidden directories.")
353                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
354                             .setDescription(StdMsgs.useMsg("--input-include-std with 'HIDDEN_DIR' argument")).get()).build()
355             )
356     ),
357 
358     /**
359      * Excludes files based on SCM exclusion file processing.
360      */
361     EXCLUDE_PARSE_SCM(new OptionGroup()
362             .addOption(Option.builder().longOpt("input-exclude-parsed-scm")
363                     .argName("StandardCollection")
364                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
365                     .desc("Parse SCM based exclusion files to exclude specified files and directories.")
366                     .type(StandardCollection.class)
367                     .build())
368     ),
369 
370     /**
371      * Stop processing an input stream and declare an input file.
372      */
373     DIR(new OptionGroup().addOption(Option.builder().option("d").longOpt("dir").hasArg()
374                     .type(File.class)
375             .desc("Used to indicate end of list when using options that take multiple arguments.").argName("DirOrArchive")
376             .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
377                     .setDescription("Use the standard '--' to signal the end of arguments.").get()).build())),
378 
379     /////////////// OUTPUT OPTIONS
380     /**
381      * Defines the stylesheet to use.
382      */
383     OUTPUT_STYLE(new OptionGroup()
384             .addOption(Option.builder().longOpt("output-style").hasArg().argName("StyleSheet")
385                     .desc("XSLT stylesheet to use when creating the report. "
386                             + "Either an external xsl file may be specified or one of the internal named sheets.")
387                     .build())
388             .addOption(Option.builder("s").longOpt("stylesheet").hasArg().argName("StyleSheet")
389                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-style")).get())
390                     .desc("XSLT stylesheet to use when creating the report.")
391                     .build())
392             .addOption(Option.builder("x").longOpt("xml")
393                     .deprecated(DeprecatedAttributes.builder()
394                             .setSince("0.17")
395                             .setForRemoval(true)
396                             .setDescription(StdMsgs.useMsg("--output-style with the 'xml' argument")).get())
397                     .desc("forces XML output rather than the textual report.")
398                     .build())),
399 
400     /**
401      * Specifies the license definitions that should be included in the output.
402      */
403     OUTPUT_LICENSES(new OptionGroup()
404             .addOption(Option.builder().longOpt("output-licenses").hasArg().argName("LicenseFilter")
405                     .desc("List the defined licenses.")
406                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
407                     .build())
408             .addOption(Option.builder().longOpt("list-licenses").hasArg().argName("LicenseFilter")
409                     .desc("List the defined licenses.")
410                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
411                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-licenses")).get())
412                     .build())),
413 
414     /**
415      * Specifies the license families that should be included in the output.
416      */
417     OUTPUT_FAMILIES(new OptionGroup()
418             .addOption(Option.builder().longOpt("output-families").hasArg().argName("LicenseFilter")
419                     .desc("List the defined license families.")
420                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
421                     .build())
422             .addOption(Option.builder().longOpt("list-families").hasArg().argName("LicenseFilter")
423                     .desc("List the defined license families.")
424                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
425                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-families")).get())
426                     .build())),
427 
428     /**
429      * Specifies the log level to log messages at.
430      */
431     LOG_LEVEL(new OptionGroup().addOption(Option.builder().longOpt("log-level")
432             .hasArg().argName("LogLevel")
433             .desc("Sets the log level.")
434             .converter(s -> Log.Level.valueOf(s.toUpperCase()))
435             .build())),
436 
437     /**
438      * Specifies that the run should not perform any updates to files.
439      */
440     DRY_RUN(new OptionGroup().addOption(Option.builder().longOpt("dry-run")
441             .desc("If set do not update the files but generate the reports.")
442             .build())),
443 
444     /**
445      * Specifies where the output should be written.
446      */
447     OUTPUT_FILE(new OptionGroup()
448             .addOption(Option.builder().option("o").longOpt("out").hasArg().argName("File")
449                     .desc("Define the output file where to write a report to.")
450                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-file")).get())
451                     .type(File.class)
452                     .converter(Converters.FILE_CONVERTER)
453                     .build())
454             .addOption(Option.builder().longOpt("output-file").hasArg().argName("File")
455                     .desc("Define the output file where to write a report to.")
456                     .type(File.class)
457                     .converter(Converters.FILE_CONVERTER)
458                     .build())),
459 
460     /**
461      * Specifies the level of reporting detail for archive files.
462      */
463     OUTPUT_ARCHIVE(new OptionGroup()
464             .addOption(Option.builder().longOpt("output-archive").hasArg().argName("ProcessingType")
465                     .desc("Specifies the level of detail in ARCHIVE file reporting.")
466                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
467                     .build())),
468 
469     /**
470      * Specifies the level of reporting detail for standard files.
471      */
472     OUTPUT_STANDARD(new OptionGroup()
473             .addOption(Option.builder().longOpt("output-standard").hasArg().argName("ProcessingType")
474                     .desc("Specifies the level of detail in STANDARD file reporting.")
475                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
476                     .build())),
477 
478     /**
479      * Provide license definition listing of registered licenses.
480      */
481     HELP_LICENSES(new OptionGroup()
482             .addOption(Option.builder().longOpt("help-licenses") //
483                     .desc("Print information about registered licenses.").build()));
484 
485     /** The option group for the argument */
486     private final OptionGroup group;
487 
488     /**
489      * Creates an Arg from an Option group.
490      *
491      * @param group The option group.
492      */
493     Arg(final OptionGroup group) {
494         this.group = group;
495     }
496 
497     /**
498      * Determines if the group has a selected element.
499      *
500      * @return {@code true} if the group has a selected element.
501      */
502     public boolean isSelected() {
503         return group.getSelected() != null;
504     }
505 
506     /**
507      * Gets the select element from the group.
508      *
509      * @return the selected element or {@code null} if no element is selected.
510      */
511     public Option getSelected() {
512         String s = group.getSelected();
513         if (s != null) {
514             for (Option result : group.getOptions()) {
515                 if (result.getKey().equals(s)) {
516                     return result;
517                 }
518             }
519         }
520         return null;
521     }
522 
523     /**
524      * Finds the element associated with the key within the element group.
525      *
526      * @param key the key to search for.
527      * @return the matching Option.
528      * @throws IllegalArgumentException if the key can not be found.
529      */
530     public Option find(final String key) {
531         for (Option result : group.getOptions()) {
532             if (key.equals(result.getKey()) || key.equals(result.getLongOpt())) {
533                 return result;
534             }
535         }
536         throw new IllegalArgumentException("Can not find " + key);
537     }
538 
539     /**
540      * Gets the default value for this arg.
541      * @return default value of this arg.
542      */
543     public String defaultValue() {
544         return DEFAULT_VALUES.get(this);
545     }
546 
547     /**
548      * Gets the group for this arg.
549      *
550      * @return the option group for this arg.
551      */
552     public OptionGroup group() {
553         return group;
554     }
555 
556     /**
557      * Returns the first non-deprecated option from the group.
558      *
559      * @return the first non-deprecated option or {@code null} if no non-deprecated option is available.
560      */
561     public Option option() {
562         for (Option result : group.getOptions()) {
563             if (!result.isDeprecated()) {
564                 return result;
565             }
566         }
567         return null;
568     }
569 
570     /**
571      * Gets the full set of options.
572      * @return  the full set of options for this Arg.
573      */
574     public static Options getOptions() {
575         Options options = new Options();
576         for (Arg arg : Arg.values()) {
577             options.addOptionGroup(arg.group);
578         }
579         return options;
580     }
581 
582     /**
583      * Processes the edit arguments.
584      *
585      * @param context the context to work with.
586      */
587     private static void processEditArgs(final ArgumentContext context) {
588         if (EDIT_ADD.isSelected()) {
589             context.getCommandLine().hasOption(Arg.EDIT_ADD.getSelected());
590             boolean force = EDIT_OVERWRITE.isSelected();
591             if (force) {
592                 context.getCommandLine().hasOption(EDIT_OVERWRITE.getSelected());
593             }
594             context.getConfiguration().setAddLicenseHeaders(force ? AddLicenseHeaders.FORCED : AddLicenseHeaders.TRUE);
595             if (EDIT_COPYRIGHT.isSelected()) {
596                 context.getConfiguration().setCopyrightMessage(context.getCommandLine().getOptionValue(EDIT_COPYRIGHT.getSelected()));
597             }
598         }
599     }
600 
601     private static List<String> processArrayArg(final ArgumentContext context, final Arg arg) throws ParseException {
602         String[] ids = context.getCommandLine().getParsedOptionValue(arg.getSelected());
603         return Arrays.asList(ids);
604     }
605 
606     private static List<String> processArrayFile(final ArgumentContext context, final Arg arg) throws ParseException {
607         List<String> result = new ArrayList<>();
608         File file = context.getCommandLine().getParsedOptionValue(arg.getSelected());
609         try (InputStream in = Files.newInputStream(file.toPath())) {
610             for (String line : IOUtils.readLines(in, StandardCharsets.UTF_8)) {
611                 String[] ids = Converters.TEXT_LIST_CONVERTER.apply(line);
612                 result.addAll(Arrays.asList(ids));
613             }
614             return result;
615         } catch (IOException e) {
616             throw new ConfigurationException(e);
617         }
618     }
619 
620     /**
621      * Processes the configuration options.
622      *
623      * @param context the context to process.
624      * @throws ConfigurationException if configuration files can not be read.
625      */
626     private static void processConfigurationArgs(final ArgumentContext context) throws ConfigurationException {
627         try {
628             Defaults.Builder defaultBuilder = Defaults.builder();
629             if (CONFIGURATION.isSelected()) {
630                 File[] files = CONFIGURATION.getParsedOptionValues(context.getCommandLine());
631                 for (File file : files) {
632                     defaultBuilder.add(file);
633                 }
634             }
635             if (CONFIGURATION_NO_DEFAULTS.isSelected()) {
636                 // display deprecation log if needed.
637                 context.getCommandLine().hasOption(CONFIGURATION_NO_DEFAULTS.getSelected());
638                 defaultBuilder.noDefault();
639             }
640             context.getConfiguration().setFrom(defaultBuilder.build());
641 
642             if (FAMILIES_APPROVED.isSelected()) {
643                 context.getConfiguration().addApprovedLicenseCategories(processArrayArg(context, FAMILIES_APPROVED));
644             }
645             if (FAMILIES_APPROVED_FILE.isSelected()) {
646                 context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, FAMILIES_APPROVED_FILE));
647             }
648             if (FAMILIES_DENIED.isSelected()) {
649                 context.getConfiguration().removeApprovedLicenseCategories(processArrayArg(context, FAMILIES_DENIED));
650             }
651             if (FAMILIES_DENIED_FILE.isSelected()) {
652                 context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, FAMILIES_DENIED_FILE));
653             }
654 
655             if (LICENSES_APPROVED.isSelected()) {
656                 context.getConfiguration().addApprovedLicenseIds(processArrayArg(context, LICENSES_APPROVED));
657             }
658 
659             if (LICENSES_APPROVED_FILE.isSelected()) {
660                 context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, LICENSES_APPROVED_FILE));
661             }
662             if (LICENSES_DENIED.isSelected()) {
663                 context.getConfiguration().removeApprovedLicenseIds(processArrayArg(context, LICENSES_DENIED));
664             }
665 
666             if (LICENSES_DENIED_FILE.isSelected()) {
667                 context.getConfiguration().removeApprovedLicenseIds(processArrayFile(context, LICENSES_DENIED_FILE));
668             }
669             if (COUNTER_MAX.isSelected()) {
670                 for (String arg : context.getCommandLine().getOptionValues(COUNTER_MAX.getSelected())) {
671                     Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
672                     int limit = pair.getValue();
673                     context.getConfiguration().getClaimValidator().setMax(pair.getKey(), limit < 0 ? Integer.MAX_VALUE : limit);
674                 }
675             }
676             if (COUNTER_MIN.isSelected()) {
677                 for (String arg : context.getCommandLine().getOptionValues(COUNTER_MIN.getSelected())) {
678                     Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
679                     context.getConfiguration().getClaimValidator().setMin(pair.getKey(), pair.getValue());
680                 }
681             }
682         } catch (Exception e) {
683             throw ConfigurationException.from(e);
684         }
685     }
686 
687     /**
688      * Process the input setup.
689      *
690      * @param context the context to work in.
691      * @throws ConfigurationException if an exclude file can not be read.
692      */
693     private static void processInputArgs(final ArgumentContext context) throws ConfigurationException {
694         try {
695             if (SOURCE.isSelected()) {
696                 File[] files = SOURCE.getParsedOptionValues(context.getCommandLine());
697                 for (File f : files) {
698                     context.getConfiguration().addSource(f);
699                 }
700             }
701             // TODO when include/exclude processing is updated check calling methods to ensure that all specified
702             // directories are handled in the list of directories.
703             if (EXCLUDE.isSelected()) {
704                 String[] excludes = context.getCommandLine().getOptionValues(EXCLUDE.getSelected());
705                 if (excludes != null) {
706                     context.getConfiguration().addExcludedPatterns(Arrays.asList(excludes));
707                 }
708             }
709             if (EXCLUDE_FILE.isSelected()) {
710                 File excludeFileName = context.getCommandLine().getParsedOptionValue(EXCLUDE_FILE.getSelected());
711                 if (excludeFileName != null) {
712                     context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName, "#"));
713                 }
714             }
715             if (EXCLUDE_STD.isSelected()) {
716                 for (String s : context.getCommandLine().getOptionValues(EXCLUDE_STD.getSelected())) {
717                     context.getConfiguration().addExcludedCollection(StandardCollection.valueOf(s));
718                 }
719             }
720             if (EXCLUDE_PARSE_SCM.isSelected()) {
721                 StandardCollection[] collections = EXCLUDE_PARSE_SCM.getParsedOptionValues(context.getCommandLine());
722                 final ReportConfiguration configuration = context.getConfiguration();
723                 for (StandardCollection collection : collections) {
724                     if (collection == StandardCollection.ALL) {
725                         Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedFileProcessor);
726                         Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedCollection);
727                     } else {
728                         configuration.addExcludedFileProcessor(collection);
729                         configuration.addExcludedCollection(collection);
730                     }
731                 }
732             }
733             if (EXCLUDE_SIZE.isSelected()) {
734                 final int maxSize = EXCLUDE_SIZE.getParsedOptionValue(context.getCommandLine());
735                 DocumentNameMatcher matcher = new DocumentNameMatcher(String.format("File size < %s bytes", maxSize),
736                         (Predicate<DocumentName>) documentName -> {
737                         File f = new File(documentName.getName());
738                         return f.isFile() && f.length() < maxSize;
739                 });
740                 context.getConfiguration().addExcludedMatcher(matcher);
741             }
742             if (INCLUDE.isSelected()) {
743                 String[] includes = context.getCommandLine().getOptionValues(INCLUDE.getSelected());
744                 if (includes != null) {
745                     context.getConfiguration().addIncludedPatterns(Arrays.asList(includes));
746                 }
747             }
748             if (INCLUDE_FILE.isSelected()) {
749                 File includeFileName = context.getCommandLine().getParsedOptionValue(INCLUDE_FILE.getSelected());
750                 if (includeFileName != null) {
751                     context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName, "#"));
752                 }
753             }
754             if (INCLUDE_STD.isSelected()) {
755                 Option selected = INCLUDE_STD.getSelected();
756                 // display deprecation log if needed.
757                 if (context.getCommandLine().hasOption("scan-hidden-directories")) {
758                     context.getConfiguration().addIncludedCollection(StandardCollection.HIDDEN_DIR);
759                 } else {
760                     for (String s : context.getCommandLine().getOptionValues(selected)) {
761                         context.getConfiguration().addIncludedCollection(StandardCollection.valueOf(s));
762                     }
763                 }
764             }
765         } catch (Exception e) {
766             throw ConfigurationException.from(e);
767         }
768     }
769 
770     /**
771      * Logs a ParseException as a warning.
772      *
773      * @param log       the Log to write to
774      * @param exception the parse exception to log
775      * @param opt       the option being processed
776      * @param cl        the command line being processed
777      * @param defaultValue      The default value the option is being set to.
778      */
779     private static void logParseException(final Log log, final ParseException exception, final Option opt, final CommandLine cl, final Object defaultValue) {
780         log.warn(format("Invalid %s specified: %s ", opt.getOpt(), cl.getOptionValue(opt)));
781         log.warn(format("%s set to: %s", opt.getOpt(), defaultValue));
782         log.debug(exception);
783     }
784 
785     /**
786      * Process the log level setting.
787      *
788      * @param commandLine The command line to process.
789      */
790     public static void processLogLevel(final CommandLine commandLine) {
791         if (LOG_LEVEL.getSelected() != null) {
792             Log dLog = DefaultLog.getInstance();
793             try {
794                 dLog.setLevel(commandLine.getParsedOptionValue(LOG_LEVEL.getSelected()));
795             } catch (ParseException e) {
796                 logParseException(DefaultLog.getInstance(), e, LOG_LEVEL.getSelected(), commandLine, dLog.getLevel());
797             }
798         }
799     }
800 
801     /**
802      * Process the arguments.
803      *
804      * @param context the context in which to process the args.
805      * @throws ConfigurationException on error
806      */
807     public static void processArgs(final ArgumentContext context) throws ConfigurationException {
808         Converters.FILE_CONVERTER.setWorkingDirectory(context.getWorkingDirectory());
809         processOutputArgs(context);
810         processEditArgs(context);
811         processInputArgs(context);
812         processConfigurationArgs(context);
813     }
814 
815     /**
816      * Process the arguments that can be processed together.
817      *
818      * @param context the context in which to process the args.
819      */
820     private static void processOutputArgs(final ArgumentContext context) {
821         context.getConfiguration().setDryRun(DRY_RUN.isSelected());
822 
823         if (OUTPUT_FAMILIES.isSelected()) {
824             try {
825                 context.getConfiguration().listFamilies(context.getCommandLine().getParsedOptionValue(OUTPUT_FAMILIES.getSelected()));
826             } catch (ParseException e) {
827                 context.logParseException(e, OUTPUT_FAMILIES.getSelected(), Defaults.LIST_FAMILIES);
828             }
829         }
830 
831         if (OUTPUT_LICENSES.isSelected()) {
832             try {
833                 context.getConfiguration().listLicenses(context.getCommandLine().getParsedOptionValue(OUTPUT_LICENSES.getSelected()));
834             } catch (ParseException e) {
835                 context.logParseException(e, OUTPUT_LICENSES.getSelected(), Defaults.LIST_LICENSES);
836             }
837         }
838 
839         if (OUTPUT_ARCHIVE.isSelected()) {
840             try {
841                 context.getConfiguration().setArchiveProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_ARCHIVE.getSelected()));
842             } catch (ParseException e) {
843                 context.logParseException(e, OUTPUT_ARCHIVE.getSelected(), Defaults.ARCHIVE_PROCESSING);
844             }
845         }
846 
847         if (OUTPUT_STANDARD.isSelected()) {
848             try {
849                 context.getConfiguration().setStandardProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_STANDARD.getSelected()));
850             } catch (ParseException e) {
851                 context.logParseException(e, OUTPUT_STANDARD.getSelected(), Defaults.STANDARD_PROCESSING);
852             }
853         }
854 
855         if (OUTPUT_FILE.isSelected()) {
856             try {
857                 File file = context.getCommandLine().getParsedOptionValue(OUTPUT_FILE.getSelected());
858                 File parent = file.getParentFile();
859                 if (!parent.mkdirs() && !parent.isDirectory()) {
860                     DefaultLog.getInstance().error("Could not create report parent directory " + file);
861                 }
862                 context.getConfiguration().setOut(file);
863             } catch (ParseException e) {
864                 context.logParseException(e, OUTPUT_FILE.getSelected(), "System.out");
865                 context.getConfiguration().setOut((IOSupplier<OutputStream>) null);
866             }
867         }
868 
869         if (OUTPUT_STYLE.isSelected()) {
870             String selected = OUTPUT_STYLE.getSelected().getKey();
871             if ("x".equals(selected)) {
872                 // display deprecated message.
873                 context.getCommandLine().hasOption("x");
874                 context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml"));
875             } else {
876                 String[] style = context.getCommandLine().getOptionValues(OUTPUT_STYLE.getSelected());
877                 if (style.length != 1) {
878                     DefaultLog.getInstance().error("Please specify a single stylesheet");
879                     throw new ConfigurationException("Please specify a single stylesheet");
880                 }
881                 context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0]));
882             }
883         }
884     }
885 
886     /**
887      * Resets the groups in the Args so that they are unused and ready to detect the next set of arguments.
888      */
889     public static void reset() {
890         for (Arg a : Arg.values()) {
891             try {
892                 a.group.setSelected(null);
893             } catch (AlreadySelectedException e) {
894                 throw new RuntimeException("Should not happen", e);
895             }
896         }
897     }
898 
899     /**
900      * Finds the Arg that an Option is in.
901      *
902      * @param optionToFind the Option to locate.
903      * @return The Arg or {@code null} if no Arg is found.
904      */
905     public static Arg findArg(final Option optionToFind) {
906         if (optionToFind != null) {
907             for (Arg arg : Arg.values()) {
908                 for (Option candidate : arg.group.getOptions()) {
909                     if (optionToFind.equals(candidate)) {
910                         return arg;
911                     }
912                 }
913             }
914         }
915         return null;
916     }
917 
918     /**
919      * Finds the Arg that contains an Option with the specified key.
920      * @param key the key for the Option to locate.
921      * @return The Arg or {@code null} if no Arg is found.
922      */
923     public static Arg findArg(final String key) {
924         if (key != null) {
925             for (Arg arg : Arg.values()) {
926                 for (Option candidate : arg.group.getOptions()) {
927                     if (key.equals(candidate.getKey()) || key.equals(candidate.getLongOpt())) {
928                         return arg;
929                     }
930                 }
931             }
932         }
933         return null;
934     }
935 
936     private <T> T getParsedOptionValue(final CommandLine commandLine) throws ParseException {
937         return commandLine.getParsedOptionValue(this.getSelected());
938     }
939 
940     private String getOptionValue(final CommandLine commandLine) {
941         return commandLine.getOptionValue(this.getSelected());
942     }
943 
944     private String[] getOptionValues(final CommandLine commandLine) {
945         return commandLine.getOptionValues(this.getSelected());
946     }
947 
948     private <T> T[] getParsedOptionValues(final CommandLine commandLine)  {
949         Option option = getSelected();
950         Class<? extends T> clazz = (Class<? extends T>) option.getType();
951         String[] values = commandLine.getOptionValues(option);
952         T[] result = (T[]) Array.newInstance(clazz, values.length);
953         try {
954             for (int i = 0; i < values.length; i++) {
955                 result[i] = clazz.cast(option.getConverter().apply(values[i]));
956             }
957             return result;
958         } catch (Throwable t) {
959             throw new ConfigurationException(format("'%s' converter for %s '%s' does not produce a class of type %s", this,
960                     option.getKey(), option.getConverter().getClass().getName(), option.getType()), t);
961         }
962     }
963 
964     /**
965      * Standard messages used in descriptions.
966      */
967     public static final class StdMsgs {
968         private StdMsgs() {
969             // do not instantiate
970         }
971 
972         /**
973          * Gets the standard "use instead" message for the specific name.
974          *
975          * @param name the name of the option to use instead.
976          * @return combined "use instead" message.
977          */
978         public static String useMsg(final String name) {
979             return format("Use %s instead.", name);
980         }
981     }
982 
983     /**
984      * The default values description map
985      */
986     private static final Map<Arg, String> DEFAULT_VALUES = new HashMap<>();
987 
988     static {
989         DEFAULT_VALUES.put(OUTPUT_FILE, "System.out");
990         DEFAULT_VALUES.put(LOG_LEVEL, Log.Level.WARN.name());
991         DEFAULT_VALUES.put(OUTPUT_ARCHIVE, Defaults.ARCHIVE_PROCESSING.name());
992         DEFAULT_VALUES.put(OUTPUT_STANDARD, Defaults.STANDARD_PROCESSING.name());
993         DEFAULT_VALUES.put(OUTPUT_LICENSES, Defaults.LIST_LICENSES.name());
994         DEFAULT_VALUES.put(OUTPUT_FAMILIES, Defaults.LIST_FAMILIES.name());
995     }
996 }