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 Apache-2.0 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 Apache-2.0 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 a 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 the 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                             "Excludes any path matcher actions but DOES NOT exclude any file processor actions.")
298                     .type(StandardCollection.class)
299                     .build())
300     ),
301 
302     /**
303      * Excludes files if they are smaller than the given threshold.
304      */
305     EXCLUDE_SIZE(new OptionGroup()
306             .addOption(Option.builder().longOpt("input-exclude-size").argName("Integer")
307                     .hasArg().type(Integer.class)
308                     .desc("Excludes files with sizes less than the number of bytes specified.")
309                     .build())
310     ),
311     /**
312      * Excludes files by expression.
313      */
314     INCLUDE(new OptionGroup()
315             .addOption(Option.builder().longOpt("input-include").hasArgs().argName("Expression")
316                     .desc("Includes files matching <Expression>. Will override excluded files.")
317                     .build())
318             .addOption(Option.builder().longOpt("include").hasArgs().argName("Expression")
319                     .desc("Includes files matching <Expression>. Will override excluded files.")
320                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
321                             .setDescription(StdMsgs.useMsg("--input-include")).get())
322                     .build())
323     ),
324 
325     /**
326      * Includes files based on the contents of a file.
327      */
328     INCLUDE_FILE(new OptionGroup()
329             .addOption(Option.builder().longOpt("input-include-file")
330                     .argName("File").hasArg().type(File.class)
331                     .converter(Converters.FILE_CONVERTER)
332                     .desc("Reads <Expression> entries from a file. Entries will override excluded files.")
333                     .build())
334             .addOption(Option.builder().longOpt("includes-file")
335                     .argName("File").hasArg().type(File.class)
336                     .converter(Converters.FILE_CONVERTER)
337                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
338                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
339                             .setDescription(StdMsgs.useMsg("--input-include-file")).get())
340                     .build())),
341 
342     /**
343      * Includes files based on standard groups.
344      */
345     INCLUDE_STD(new OptionGroup()
346             .addOption(Option.builder().longOpt("input-include-std").argName("StandardCollection")
347                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
348                     .desc("Includes files defined in standard collections based on commonly occurring groups. " +
349                             "Includes any path matcher actions but DOES NOT include any file processor actions.")
350                     .type(StandardCollection.class)
351                     .build())
352             .addOption(Option.builder().longOpt("scan-hidden-directories")
353                     .desc("Scans hidden directories.")
354                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
355                             .setDescription(StdMsgs.useMsg("--input-include-std with 'HIDDEN_DIR' argument")).get()).build()
356             )
357     ),
358 
359     /**
360      * Excludes files based on SCM exclusion file processing.
361      */
362     EXCLUDE_PARSE_SCM(new OptionGroup()
363             .addOption(Option.builder().longOpt("input-exclude-parsed-scm")
364                     .argName("StandardCollection")
365                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
366                     .desc("Parse SCM based exclusion files to exclude specified files and directories. " +
367                             "This action can apply to any standard collection that implements a file processor.")
368                     .type(StandardCollection.class)
369                     .build())
370     ),
371 
372     /**
373      * Stop processing an input stream and declare an input file.
374      */
375     DIR(new OptionGroup().addOption(Option.builder().option("d").longOpt("dir").hasArg()
376                     .type(File.class)
377             .desc("Used to indicate end of list when using options that take multiple arguments.").argName("DirOrArchive")
378             .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
379                     .setDescription("Use the standard '--' to signal the end of arguments.").get()).build())),
380 
381     /////////////// OUTPUT OPTIONS
382     /**
383      * Defines the stylesheet to use.
384      */
385     OUTPUT_STYLE(new OptionGroup()
386             .addOption(Option.builder().longOpt("output-style").hasArg().argName("StyleSheet")
387                     .desc("XSLT stylesheet to use when creating the report. "
388                             + "Either an external xsl file may be specified or one of the internal named sheets.")
389                     .build())
390             .addOption(Option.builder("s").longOpt("stylesheet").hasArg().argName("StyleSheet")
391                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-style")).get())
392                     .desc("XSLT stylesheet to use when creating the report.")
393                     .build())
394             .addOption(Option.builder("x").longOpt("xml")
395                     .deprecated(DeprecatedAttributes.builder()
396                             .setSince("0.17")
397                             .setForRemoval(true)
398                             .setDescription(StdMsgs.useMsg("--output-style with the 'xml' argument")).get())
399                     .desc("forces XML output rather than the textual report.")
400                     .build())),
401 
402     /**
403      * Specifies the license definitions that should be included in the output.
404      */
405     OUTPUT_LICENSES(new OptionGroup()
406             .addOption(Option.builder().longOpt("output-licenses").hasArg().argName("LicenseFilter")
407                     .desc("List the defined licenses.")
408                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
409                     .build())
410             .addOption(Option.builder().longOpt("list-licenses").hasArg().argName("LicenseFilter")
411                     .desc("List the defined licenses.")
412                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
413                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-licenses")).get())
414                     .build())),
415 
416     /**
417      * Specifies the license families that should be included in the output.
418      */
419     OUTPUT_FAMILIES(new OptionGroup()
420             .addOption(Option.builder().longOpt("output-families").hasArg().argName("LicenseFilter")
421                     .desc("List the defined license families.")
422                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
423                     .build())
424             .addOption(Option.builder().longOpt("list-families").hasArg().argName("LicenseFilter")
425                     .desc("List the defined license families.")
426                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
427                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-families")).get())
428                     .build())),
429 
430     /**
431      * Specifies the log level to log messages at.
432      */
433     LOG_LEVEL(new OptionGroup().addOption(Option.builder().longOpt("log-level")
434             .hasArg().argName("LogLevel")
435             .desc("Sets the log level.")
436             .converter(s -> Log.Level.valueOf(s.toUpperCase()))
437             .build())),
438 
439     /**
440      * Specifies that the run should not perform any updates to files.
441      */
442     DRY_RUN(new OptionGroup().addOption(Option.builder().longOpt("dry-run")
443             .desc("If set do not update the files but generate the reports.")
444             .build())),
445 
446     /**
447      * Specifies where the output should be written.
448      */
449     OUTPUT_FILE(new OptionGroup()
450             .addOption(Option.builder().option("o").longOpt("out").hasArg().argName("File")
451                     .desc("Define the output file where to write a report to.")
452                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-file")).get())
453                     .type(File.class)
454                     .converter(Converters.FILE_CONVERTER)
455                     .build())
456             .addOption(Option.builder().longOpt("output-file").hasArg().argName("File")
457                     .desc("Define the output file where to write a report to.")
458                     .type(File.class)
459                     .converter(Converters.FILE_CONVERTER)
460                     .build())),
461 
462     /**
463      * Specifies the level of reporting detail for archive files.
464      */
465     OUTPUT_ARCHIVE(new OptionGroup()
466             .addOption(Option.builder().longOpt("output-archive").hasArg().argName("ProcessingType")
467                     .desc("Specifies the level of detail in ARCHIVE file reporting.")
468                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
469                     .build())),
470 
471     /**
472      * Specifies the level of reporting detail for standard files.
473      */
474     OUTPUT_STANDARD(new OptionGroup()
475             .addOption(Option.builder().longOpt("output-standard").hasArg().argName("ProcessingType")
476                     .desc("Specifies the level of detail in STANDARD file reporting.")
477                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
478                     .build())),
479 
480     /**
481      * Provide license definition listing of registered licenses.
482      */
483     HELP_LICENSES(new OptionGroup()
484             .addOption(Option.builder().longOpt("help-licenses") //
485                     .desc("Print information about registered licenses.").build()));
486 
487     /** The option group for the argument */
488     private final OptionGroup group;
489 
490     /**
491      * Creates an Arg from an Option group.
492      *
493      * @param group The option group.
494      */
495     Arg(final OptionGroup group) {
496         this.group = group;
497     }
498 
499     /**
500      * Determines if the group has a selected element.
501      *
502      * @return {@code true} if the group has a selected element.
503      */
504     public boolean isSelected() {
505         return group.getSelected() != null;
506     }
507 
508     /**
509      * Gets the select element from the group.
510      *
511      * @return the selected element or {@code null} if no element is selected.
512      */
513     public Option getSelected() {
514         String s = group.getSelected();
515         if (s != null) {
516             for (Option result : group.getOptions()) {
517                 if (result.getKey().equals(s)) {
518                     return result;
519                 }
520             }
521         }
522         return null;
523     }
524 
525     /**
526      * Finds the element associated with the key within the element group.
527      *
528      * @param key the key to search for.
529      * @return the matching Option.
530      * @throws IllegalArgumentException if the key can not be found.
531      */
532     public Option find(final String key) {
533         for (Option result : group.getOptions()) {
534             if (key.equals(result.getKey()) || key.equals(result.getLongOpt())) {
535                 return result;
536             }
537         }
538         throw new IllegalArgumentException("Can not find " + key);
539     }
540 
541     /**
542      * Gets the default value for this arg.
543      * @return default value of this arg.
544      */
545     public String defaultValue() {
546         return DEFAULT_VALUES.get(this);
547     }
548 
549     /**
550      * Gets the group for this arg.
551      *
552      * @return the option group for this arg.
553      */
554     public OptionGroup group() {
555         return group;
556     }
557 
558     /**
559      * Returns the first non-deprecated option from the group.
560      *
561      * @return the first non-deprecated option or {@code null} if no non-deprecated option is available.
562      */
563     public Option option() {
564         for (Option result : group.getOptions()) {
565             if (!result.isDeprecated()) {
566                 return result;
567             }
568         }
569         return null;
570     }
571 
572     /**
573      * Gets the full set of options.
574      * @return  the full set of options for this Arg.
575      */
576     public static Options getOptions() {
577         Options options = new Options();
578         for (Arg arg : Arg.values()) {
579             options.addOptionGroup(arg.group);
580         }
581         return options;
582     }
583 
584     /**
585      * Processes the edit arguments.
586      *
587      * @param context the context to work with.
588      */
589     private static void processEditArgs(final ArgumentContext context) {
590         if (EDIT_ADD.isSelected()) {
591             context.getCommandLine().hasOption(Arg.EDIT_ADD.getSelected());
592             boolean force = EDIT_OVERWRITE.isSelected();
593             if (force) {
594                 context.getCommandLine().hasOption(EDIT_OVERWRITE.getSelected());
595             }
596             context.getConfiguration().setAddLicenseHeaders(force ? AddLicenseHeaders.FORCED : AddLicenseHeaders.TRUE);
597             if (EDIT_COPYRIGHT.isSelected()) {
598                 context.getConfiguration().setCopyrightMessage(context.getCommandLine().getOptionValue(EDIT_COPYRIGHT.getSelected()));
599             }
600         }
601     }
602 
603     private static List<String> processArrayArg(final ArgumentContext context, final Arg arg) throws ParseException {
604         String[] ids = context.getCommandLine().getParsedOptionValue(arg.getSelected());
605         return Arrays.asList(ids);
606     }
607 
608     private static List<String> processArrayFile(final ArgumentContext context, final Arg arg) throws ParseException {
609         List<String> result = new ArrayList<>();
610         File file = context.getCommandLine().getParsedOptionValue(arg.getSelected());
611         try (InputStream in = Files.newInputStream(file.toPath())) {
612             for (String line : IOUtils.readLines(in, StandardCharsets.UTF_8)) {
613                 String[] ids = Converters.TEXT_LIST_CONVERTER.apply(line);
614                 result.addAll(Arrays.asList(ids));
615             }
616             return result;
617         } catch (IOException e) {
618             throw new ConfigurationException(e);
619         }
620     }
621 
622     /**
623      * Processes the configuration options.
624      *
625      * @param context the context to process.
626      * @throws ConfigurationException if configuration files can not be read.
627      */
628     private static void processConfigurationArgs(final ArgumentContext context) throws ConfigurationException {
629         try {
630             Defaults.Builder defaultBuilder = Defaults.builder();
631             if (CONFIGURATION.isSelected()) {
632                 File[] files = CONFIGURATION.getParsedOptionValues(context.getCommandLine());
633                 for (File file : files) {
634                     defaultBuilder.add(file);
635                 }
636             }
637             if (CONFIGURATION_NO_DEFAULTS.isSelected()) {
638                 // display deprecation log if needed.
639                 context.getCommandLine().hasOption(CONFIGURATION_NO_DEFAULTS.getSelected());
640                 defaultBuilder.noDefault();
641             }
642             context.getConfiguration().setFrom(defaultBuilder.build());
643 
644             if (FAMILIES_APPROVED.isSelected()) {
645                 context.getConfiguration().addApprovedLicenseCategories(processArrayArg(context, FAMILIES_APPROVED));
646             }
647             if (FAMILIES_APPROVED_FILE.isSelected()) {
648                 context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, FAMILIES_APPROVED_FILE));
649             }
650             if (FAMILIES_DENIED.isSelected()) {
651                 context.getConfiguration().removeApprovedLicenseCategories(processArrayArg(context, FAMILIES_DENIED));
652             }
653             if (FAMILIES_DENIED_FILE.isSelected()) {
654                 context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, FAMILIES_DENIED_FILE));
655             }
656 
657             if (LICENSES_APPROVED.isSelected()) {
658                 context.getConfiguration().addApprovedLicenseIds(processArrayArg(context, LICENSES_APPROVED));
659             }
660 
661             if (LICENSES_APPROVED_FILE.isSelected()) {
662                 context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, LICENSES_APPROVED_FILE));
663             }
664             if (LICENSES_DENIED.isSelected()) {
665                 context.getConfiguration().removeApprovedLicenseIds(processArrayArg(context, LICENSES_DENIED));
666             }
667 
668             if (LICENSES_DENIED_FILE.isSelected()) {
669                 context.getConfiguration().removeApprovedLicenseIds(processArrayFile(context, LICENSES_DENIED_FILE));
670             }
671             if (COUNTER_MAX.isSelected()) {
672                 for (String arg : context.getCommandLine().getOptionValues(COUNTER_MAX.getSelected())) {
673                     Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
674                     int limit = pair.getValue();
675                     context.getConfiguration().getClaimValidator().setMax(pair.getKey(), limit < 0 ? Integer.MAX_VALUE : limit);
676                 }
677             }
678             if (COUNTER_MIN.isSelected()) {
679                 for (String arg : context.getCommandLine().getOptionValues(COUNTER_MIN.getSelected())) {
680                     Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
681                     context.getConfiguration().getClaimValidator().setMin(pair.getKey(), pair.getValue());
682                 }
683             }
684         } catch (Exception e) {
685             throw ConfigurationException.from(e);
686         }
687     }
688 
689     /**
690      * Process the input setup.
691      *
692      * @param context the context to work in.
693      * @throws ConfigurationException if an exclude file can not be read.
694      */
695     private static void processInputArgs(final ArgumentContext context) throws ConfigurationException {
696         try {
697             if (SOURCE.isSelected()) {
698                 File[] files = SOURCE.getParsedOptionValues(context.getCommandLine());
699                 for (File f : files) {
700                     context.getConfiguration().addSource(f);
701                 }
702             }
703             // TODO when include/exclude processing is updated check calling methods to ensure that all specified
704             // directories are handled in the list of directories.
705             if (EXCLUDE.isSelected()) {
706                 String[] excludes = context.getCommandLine().getOptionValues(EXCLUDE.getSelected());
707                 if (excludes != null) {
708                     context.getConfiguration().addExcludedPatterns(Arrays.asList(excludes));
709                 }
710             }
711             if (EXCLUDE_FILE.isSelected()) {
712                 File excludeFileName = context.getCommandLine().getParsedOptionValue(EXCLUDE_FILE.getSelected());
713                 if (excludeFileName != null) {
714                     context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName, "#"));
715                 }
716             }
717             if (EXCLUDE_STD.isSelected()) {
718                 for (String s : context.getCommandLine().getOptionValues(EXCLUDE_STD.getSelected())) {
719                     context.getConfiguration().addExcludedCollection(StandardCollection.valueOf(s));
720                 }
721             }
722             if (EXCLUDE_PARSE_SCM.isSelected()) {
723                 StandardCollection[] collections = EXCLUDE_PARSE_SCM.getParsedOptionValues(context.getCommandLine());
724                 final ReportConfiguration configuration = context.getConfiguration();
725                 for (StandardCollection collection : collections) {
726                     if (collection == StandardCollection.ALL) {
727                         Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedFileProcessor);
728                         Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedCollection);
729                     } else {
730                         configuration.addExcludedFileProcessor(collection);
731                         configuration.addExcludedCollection(collection);
732                     }
733                 }
734             }
735             if (EXCLUDE_SIZE.isSelected()) {
736                 final int maxSize = EXCLUDE_SIZE.getParsedOptionValue(context.getCommandLine());
737                 DocumentNameMatcher matcher = new DocumentNameMatcher(String.format("File size < %s bytes", maxSize),
738                         (Predicate<DocumentName>) documentName -> {
739                         File f = new File(documentName.getName());
740                         return f.isFile() && f.length() < maxSize;
741                 });
742                 context.getConfiguration().addExcludedMatcher(matcher);
743             }
744             if (INCLUDE.isSelected()) {
745                 String[] includes = context.getCommandLine().getOptionValues(INCLUDE.getSelected());
746                 if (includes != null) {
747                     context.getConfiguration().addIncludedPatterns(Arrays.asList(includes));
748                 }
749             }
750             if (INCLUDE_FILE.isSelected()) {
751                 File includeFileName = context.getCommandLine().getParsedOptionValue(INCLUDE_FILE.getSelected());
752                 if (includeFileName != null) {
753                     context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName, "#"));
754                 }
755             }
756             if (INCLUDE_STD.isSelected()) {
757                 Option selected = INCLUDE_STD.getSelected();
758                 // display deprecation log if needed.
759                 if (context.getCommandLine().hasOption("scan-hidden-directories")) {
760                     context.getConfiguration().addIncludedCollection(StandardCollection.HIDDEN_DIR);
761                 } else {
762                     for (String s : context.getCommandLine().getOptionValues(selected)) {
763                         context.getConfiguration().addIncludedCollection(StandardCollection.valueOf(s));
764                     }
765                 }
766             }
767         } catch (Exception e) {
768             throw ConfigurationException.from(e);
769         }
770     }
771 
772     /**
773      * Logs a ParseException as a warning.
774      *
775      * @param log       the Log to write to
776      * @param exception the parse exception to log
777      * @param opt       the option being processed
778      * @param cl        the command line being processed
779      * @param defaultValue      The default value the option is being set to.
780      */
781     private static void logParseException(final Log log, final ParseException exception, final Option opt, final CommandLine cl, final Object defaultValue) {
782         log.warn(format("Invalid %s specified: %s ", opt.getOpt(), cl.getOptionValue(opt)));
783         log.warn(format("%s set to: %s", opt.getOpt(), defaultValue));
784         log.debug(exception);
785     }
786 
787     /**
788      * Process the log level setting.
789      *
790      * @param commandLine The command line to process.
791      */
792     public static void processLogLevel(final CommandLine commandLine) {
793         if (LOG_LEVEL.getSelected() != null) {
794             Log dLog = DefaultLog.getInstance();
795             try {
796                 dLog.setLevel(commandLine.getParsedOptionValue(LOG_LEVEL.getSelected()));
797             } catch (ParseException e) {
798                 logParseException(DefaultLog.getInstance(), e, LOG_LEVEL.getSelected(), commandLine, dLog.getLevel());
799             }
800         }
801     }
802 
803     /**
804      * Process the arguments.
805      *
806      * @param context the context in which to process the args.
807      * @throws ConfigurationException on error
808      */
809     public static void processArgs(final ArgumentContext context) throws ConfigurationException {
810         Converters.FILE_CONVERTER.setWorkingDirectory(context.getWorkingDirectory());
811         processOutputArgs(context);
812         processEditArgs(context);
813         processInputArgs(context);
814         processConfigurationArgs(context);
815     }
816 
817     /**
818      * Process the arguments that can be processed together.
819      *
820      * @param context the context in which to process the args.
821      */
822     private static void processOutputArgs(final ArgumentContext context) {
823         context.getConfiguration().setDryRun(DRY_RUN.isSelected());
824 
825         if (OUTPUT_FAMILIES.isSelected()) {
826             try {
827                 context.getConfiguration().listFamilies(context.getCommandLine().getParsedOptionValue(OUTPUT_FAMILIES.getSelected()));
828             } catch (ParseException e) {
829                 context.logParseException(e, OUTPUT_FAMILIES.getSelected(), Defaults.LIST_FAMILIES);
830             }
831         }
832 
833         if (OUTPUT_LICENSES.isSelected()) {
834             try {
835                 context.getConfiguration().listLicenses(context.getCommandLine().getParsedOptionValue(OUTPUT_LICENSES.getSelected()));
836             } catch (ParseException e) {
837                 context.logParseException(e, OUTPUT_LICENSES.getSelected(), Defaults.LIST_LICENSES);
838             }
839         }
840 
841         if (OUTPUT_ARCHIVE.isSelected()) {
842             try {
843                 context.getConfiguration().setArchiveProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_ARCHIVE.getSelected()));
844             } catch (ParseException e) {
845                 context.logParseException(e, OUTPUT_ARCHIVE.getSelected(), Defaults.ARCHIVE_PROCESSING);
846             }
847         }
848 
849         if (OUTPUT_STANDARD.isSelected()) {
850             try {
851                 context.getConfiguration().setStandardProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_STANDARD.getSelected()));
852             } catch (ParseException e) {
853                 context.logParseException(e, OUTPUT_STANDARD.getSelected(), Defaults.STANDARD_PROCESSING);
854             }
855         }
856 
857         if (OUTPUT_FILE.isSelected()) {
858             try {
859                 File file = context.getCommandLine().getParsedOptionValue(OUTPUT_FILE.getSelected());
860                 File parent = file.getParentFile();
861                 if (!parent.mkdirs() && !parent.isDirectory()) {
862                     DefaultLog.getInstance().error("Could not create report parent directory " + file);
863                 }
864                 context.getConfiguration().setOut(file);
865             } catch (ParseException e) {
866                 context.logParseException(e, OUTPUT_FILE.getSelected(), "System.out");
867                 context.getConfiguration().setOut((IOSupplier<OutputStream>) null);
868             }
869         }
870 
871         if (OUTPUT_STYLE.isSelected()) {
872             String selected = OUTPUT_STYLE.getSelected().getKey(); // is not null due to above isSelected()-call
873             if ("x".equals(selected)) {
874                 // display deprecated message.
875                 context.getCommandLine().hasOption("x");
876                 context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml"));
877             } else {
878                 String[] style = context.getCommandLine().getOptionValues(OUTPUT_STYLE.getSelected());
879                 if (style.length != 1) {
880                     DefaultLog.getInstance().error("Please specify a single stylesheet");
881                     throw new ConfigurationException("Please specify a single stylesheet");
882                 }
883                 context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0]));
884             }
885         }
886     }
887 
888     /**
889      * Resets the groups in the Args so that they are unused and ready to detect the next set of arguments.
890      */
891     public static void reset() {
892         for (Arg a : Arg.values()) {
893             try {
894                 a.group.setSelected(null);
895             } catch (AlreadySelectedException e) {
896                 throw new RuntimeException("Should not happen", e);
897             }
898         }
899     }
900 
901     /**
902      * Finds the Arg that an Option is in.
903      *
904      * @param optionToFind the Option to locate.
905      * @return The Arg or {@code null} if no Arg is found.
906      */
907     public static Arg findArg(final Option optionToFind) {
908         if (optionToFind != null) {
909             for (Arg arg : Arg.values()) {
910                 for (Option candidate : arg.group.getOptions()) {
911                     if (optionToFind.equals(candidate)) {
912                         return arg;
913                     }
914                 }
915             }
916         }
917         return null;
918     }
919 
920     /**
921      * Finds the Arg that contains an Option with the specified key.
922      * @param key the key for the Option to locate.
923      * @return The Arg or {@code null} if no Arg is found.
924      */
925     public static Arg findArg(final String key) {
926         if (key != null) {
927             for (Arg arg : Arg.values()) {
928                 for (Option candidate : arg.group.getOptions()) {
929                     if (key.equals(candidate.getKey()) || key.equals(candidate.getLongOpt())) {
930                         return arg;
931                     }
932                 }
933             }
934         }
935         return null;
936     }
937 
938     private <T> T getParsedOptionValue(final CommandLine commandLine) throws ParseException {
939         return commandLine.getParsedOptionValue(this.getSelected());
940     }
941 
942     private String getOptionValue(final CommandLine commandLine) {
943         return commandLine.getOptionValue(this.getSelected());
944     }
945 
946     private String[] getOptionValues(final CommandLine commandLine) {
947         return commandLine.getOptionValues(this.getSelected());
948     }
949 
950     private <T> T[] getParsedOptionValues(final CommandLine commandLine)  {
951         Option option = getSelected();
952         try {
953             Class<? extends T> clazz = (Class<? extends T>) option.getType();
954             String[] values = commandLine.getOptionValues(option);
955             T[] result = (T[]) Array.newInstance(clazz, values.length);
956             for (int i = 0; i < values.length; i++) {
957                 result[i] = clazz.cast(option.getConverter().apply(values[i]));
958             }
959             return result;
960         } catch (Throwable t) {
961             throw new ConfigurationException(format("'%s' converter for %s '%s' does not produce a class of type %s", this,
962                     option.getKey(), option.getConverter().getClass().getName(), option.getType()), t);
963         }
964     }
965 
966     /**
967      * Standard messages used in descriptions.
968      */
969     public static final class StdMsgs {
970         private StdMsgs() {
971             // do not instantiate
972         }
973 
974         /**
975          * Gets the standard "use instead" message for the specific name.
976          *
977          * @param name the name of the option to use instead.
978          * @return combined "use instead" message.
979          */
980         public static String useMsg(final String name) {
981             return format("Use %s instead.", name);
982         }
983     }
984 
985     /**
986      * The default values description map
987      */
988     private static final Map<Arg, String> DEFAULT_VALUES = new HashMap<>();
989 
990     static {
991         DEFAULT_VALUES.put(OUTPUT_FILE, "System.out");
992         DEFAULT_VALUES.put(LOG_LEVEL, Log.Level.WARN.name());
993         DEFAULT_VALUES.put(OUTPUT_ARCHIVE, Defaults.ARCHIVE_PROCESSING.name());
994         DEFAULT_VALUES.put(OUTPUT_STANDARD, Defaults.STANDARD_PROCESSING.name());
995         DEFAULT_VALUES.put(OUTPUT_LICENSES, Defaults.LIST_LICENSES.name());
996         DEFAULT_VALUES.put(OUTPUT_FAMILIES, Defaults.LIST_FAMILIES.name());
997     }
998 }