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