This is an automated email from the ASF dual-hosted git repository. claude pushed a commit to branch create-UIOptionCollections in repository https://gitbox.apache.org/repos/asf/creadur-rat.git
commit 3e47c4326df52e397032f7822426b6542f0b7b29 Author: Claude Warren <[email protected]> AuthorDate: Sun Mar 22 08:21:54 2026 +0000 initial working code --- apache-rat-core/pom.xml | 25 +- .../src/main/java/org/apache/rat}/CLIOption.java | 20 +- .../java/org/apache/rat/CLIOptionCollection.java | 41 ++ .../main/java/org/apache/rat/OptionCollection.java | 12 +- ...Collection.java => OptionCollectionParser.java} | 156 ++--- .../main/java/org/apache/rat/commandline/Arg.java | 703 ++++++++++----------- .../apache/rat/commandline/ArgumentContext.java | 7 +- .../java/org/apache/rat/help/AbstractHelp.java | 5 +- .../org/apache/rat/ui/AbstractCodeGenerator.java | 151 +++++ .../java/org/apache/rat/ui/ArgumentTracker.java | 239 +++++++ .../src/main/java/org/apache/rat/ui/UI.java | 36 ++ .../src/main/java/org/apache/rat/ui/UIOption.java | 224 +++++++ .../java/org/apache/rat/ui/UIOptionCollection.java | 299 +++++++++ .../org/apache/rat/ui/UpdatableOptionGroup.java | 85 +++ .../rat/ui/UpdatableOptionGroupCollection.java | 110 ++++ .../main/java/org/apache/rat/ui/package-info.java | 23 + .../java/org/apache/rat/ui/spi/UIProvider.java | 25 + .../java/org/apache/rat/ui/spi/package-info.java | 22 + .../java/org/apache/rat/commandline/ArgTests.java | 3 +- .../org/apache/rat/ui/ArgumentTrackerTest.java | 122 ++++ .../org/apache/rat/ui/UIOptionCollectionTest.java | 160 +++++ .../rat/documentation/options/CLIOption.java | 2 +- pom.xml | 10 + 23 files changed, 1983 insertions(+), 497 deletions(-) diff --git a/apache-rat-core/pom.xml b/apache-rat-core/pom.xml index 4819f23b..2cce9a2a 100644 --- a/apache-rat-core/pom.xml +++ b/apache-rat-core/pom.xml @@ -37,7 +37,6 @@ <directory>src/main/filtered-resources</directory> </resource> </resources> - <pluginManagement> <plugins> <plugin> <groupId>org.apache.rat</groupId> @@ -56,9 +55,6 @@ </inputExcludes> </configuration> </plugin> - </plugins> - </pluginManagement> - <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> @@ -177,8 +173,20 @@ </build> <dependencies> <dependency> - <groupId>org.apache.rat</groupId> - <artifactId>apache-rat-testdata</artifactId> + <groupId>com.github.spotbugs</groupId> + <artifactId>spotbugs-annotations</artifactId> + </dependency> + <dependency> + <groupId>org.apache.velocity</groupId> + <artifactId>velocity-engine-core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.velocity.tools</groupId> + <artifactId>velocity-tools-generic</artifactId> + </dependency> + <dependency> + <groupId>org.reflections</groupId> + <artifactId>reflections</artifactId> <scope>test</scope> </dependency> <dependency> @@ -213,11 +221,6 @@ <artifactId>junit-jupiter-api</artifactId> <scope>test</scope> </dependency> - <dependency> - <groupId>org.junit.vintage</groupId> - <artifactId>junit-vintage-engine</artifactId> - <scope>test</scope> - </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> diff --git a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java b/apache-rat-core/src/main/java/org/apache/rat/CLIOption.java similarity index 81% copy from apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java copy to apache-rat-core/src/main/java/org/apache/rat/CLIOption.java index fc00e5e1..03b79044 100644 --- a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java +++ b/apache-rat-core/src/main/java/org/apache/rat/CLIOption.java @@ -16,19 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.rat.documentation.options; +package org.apache.rat; import org.apache.commons.cli.Option; import org.apache.commons.lang3.StringUtils; +import org.apache.rat.ui.ArgumentTracker; +import org.apache.rat.ui.UIOption; +import org.apache.rat.ui.UIOptionCollection; -public class CLIOption extends AbstractOption { - - public static String createName(final Option option) { - return StringUtils.defaultIfBlank(option.getLongOpt(), option.getOpt()); - } +/** + * The CLI option definition. + */ +public final class CLIOption extends UIOption<CLIOption> { - public CLIOption(final Option option) { - super(option, createName(option)); + public CLIOption(final UIOptionCollection<CLIOption> collection, final Option option) { + super(collection, option, ArgumentTracker.extractKey(option)); } @Override @@ -47,7 +49,7 @@ public class CLIOption extends AbstractOption { @Override protected String cleanupName(final Option option) { - return createName(option); + return ArgumentTracker.extractKey(option); } @Override diff --git a/apache-rat-core/src/main/java/org/apache/rat/CLIOptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/CLIOptionCollection.java new file mode 100644 index 00000000..feb1cbf5 --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/CLIOptionCollection.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat; + +import org.apache.commons.cli.Option; +import org.apache.rat.ui.UIOptionCollection; + +public final class CLIOptionCollection extends UIOptionCollection<CLIOption> { + /** The Help option */ + static final Option HELP = new Option("?", "help", false, "Print help for the RAT command line interface and exit."); + + /** The instance of the collection */ + public static final CLIOptionCollection INSTANCE = new CLIOptionCollection(); + + private CLIOptionCollection() { + super(new Builder().uiOption(HELP) + .mapper(CLIOption::new)); + } + + private static final class Builder extends UIOptionCollection.Builder<CLIOption, Builder> { + private Builder() { + super(); + } + } +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java index 6ce2e1c2..b16ebd09 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java +++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java @@ -140,9 +140,8 @@ public final class OptionCollection { // for "commandLine" } - Arg.processLogLevel(commandLine); - ArgumentContext argumentContext = new ArgumentContext(workingDirectory, commandLine); + Arg.processLogLevel(argumentContext, CLIOptionCollection.INSTANCE); if (commandLine.hasOption(HELP)) { helpCmd.accept(opts); @@ -175,14 +174,15 @@ public final class OptionCollection { * @see #parseCommands(File, String[], Consumer, boolean) */ static ReportConfiguration createConfiguration(final ArgumentContext argumentContext) { - argumentContext.processArgs(); + argumentContext.processArgs(CLIOptionCollection.INSTANCE); final ReportConfiguration configuration = argumentContext.getConfiguration(); final CommandLine commandLine = argumentContext.getCommandLine(); - if (Arg.DIR.isSelected()) { + if (CLIOptionCollection.INSTANCE.isSelected(Arg.DIR)) { try { - configuration.addSource(getReportable(commandLine.getParsedOptionValue(Arg.DIR.getSelected()), configuration)); + configuration.addSource(getReportable(commandLine.getParsedOptionValue( + CLIOptionCollection.INSTANCE.getSelected(Arg.DIR).get()), configuration)); } catch (ParseException e) { - throw new ConfigurationException("Unable to set parse " + Arg.DIR.getSelected(), e); + throw new ConfigurationException("Unable to set parse " + CLIOptionCollection.INSTANCE.getSelected(Arg.DIR).get(), e); } } for (String s : commandLine.getArgs()) { diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java similarity index 70% copy from apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java copy to apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java index 6ce2e1c2..5798b738 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java +++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java @@ -21,14 +21,11 @@ package org.apache.rat; import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.io.Serial; import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; -import java.util.Map; -import java.util.TreeMap; -import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -49,49 +46,32 @@ import org.apache.rat.help.Licenses; import org.apache.rat.license.LicenseSetFactory; import org.apache.rat.report.IReportable; import org.apache.rat.report.claim.ClaimStatistic; +import org.apache.rat.ui.UIOptionCollection; import org.apache.rat.utils.DefaultLog; import org.apache.rat.utils.Log.Level; import org.apache.rat.walker.ArchiveWalker; import org.apache.rat.walker.DirectoryWalker; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import static java.lang.String.format; /** - * The collection of standard options for the CLI as well as utility methods to manage them and methods to create the - * ReportConfiguration from the options and an array of arguments. + * Uses the AbstractOptionCollection to parse the command line options. + * contains utility methods to ReportConfiguration from the options and an array of arguments. */ -public final class OptionCollection { +@SuppressFBWarnings("EI_EXPOSE_REP2") +public final class OptionCollectionParser { + /** The OptionCollection that we are working with */ + private final UIOptionCollection<?> uiOptionCollection; - private OptionCollection() { - // do not instantiate + public OptionCollectionParser(final UIOptionCollection<?> optionCollection) { + this.uiOptionCollection = optionCollection; } /** The Option comparator to sort the help */ public static final Comparator<Option> OPTION_COMPARATOR = new OptionComparator(); - /** The Help option */ - public static final Option HELP = new Option("?", "help", false, "Print help for the RAT command line interface and exit."); - - /** A mapping of {@code argName(value)} values to a description of those values. */ - @Deprecated - private static final Map<String, Supplier<String>> ARGUMENT_TYPES; - static { - ARGUMENT_TYPES = new TreeMap<>(); - for (ArgumentType argType : ArgumentType.values()) { - ARGUMENT_TYPES.put(argType.getDisplayName(), argType.description); - } - } - - /** - * Gets the mapping of {@code argName(value)} values to a description of those values. - * @return the mapping of {@code argName(value)} values to a description of those values. - * @deprecated use {@link ArgumentType} - */ - @Deprecated - public static Map<String, Supplier<String>> getArgumentTypes() { - return Collections.unmodifiableMap(ARGUMENT_TYPES); - } - /** * Join a collection of objects together as a comma separated list of their string values. * @param args the objects to join together. @@ -106,63 +86,58 @@ public final class OptionCollection { * * @param workingDirectory The directory to resolve relative file names against. * @param args the arguments to parse - * @param helpCmd the help command to run when necessary. - * @return a ReportConfiguration or {@code null} if Help was printed. + * @return the ArgumentContext for the process. * @throws IOException on error. + * @throws ParseException on option parsing error. */ - public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args, final Consumer<Options> helpCmd) throws IOException { - return parseCommands(workingDirectory, args, helpCmd, false); + public ArgumentContext parseCommands(final File workingDirectory, final String[] args) + throws IOException, ParseException { + return parseCommands(workingDirectory, args, uiOptionCollection.getOptions()); } /** - * Parses the standard options to create a ReportConfiguration. - * - * @param workingDirectory The directory to resolve relative file names against. - * @param args the arguments to parse. - * @param helpCmd the help command to run when necessary. - * @param noArgs If {@code true} then the commands do not need extra arguments. - * @return a ReportConfiguration or {@code null} if Help was printed. - * @throws IOException on error. + * Parse the options into the command line. + * @param opts the option definitions. + * @param args the argument to apply the definitions to. + * @return the CommandLine + * @throws ParseException on option parsing error. */ - public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args, - final Consumer<Options> helpCmd, final boolean noArgs) throws IOException { - - Options opts = buildOptions(); - CommandLine commandLine; + //@VisibleForTesting + CommandLine parseCommandLine(final Options opts, final String[] args) throws ParseException { try { - commandLine = DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter()) + return DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter()) .setAllowPartialMatching(true).build().parse(opts, args); } catch (ParseException e) { DefaultLog.getInstance().error(e.getMessage()); DefaultLog.getInstance().error("Please use the \"--help\" option to see a list of valid commands and options.", e); - System.exit(1); - return null; // dummy return (won't be reached) to avoid Eclipse complaint about possible NPE - // for "commandLine" + throw e; } + } - Arg.processLogLevel(commandLine); + /** + * Parses the standard options to create a ReportConfiguration. + * + * @param workingDirectory The directory to resolve relative file names against. + * @param args the arguments to parse. + * @param options An Options object containing Apache command line options. + * @return the ArgumentContext for the process. + * @throws IOException on error. + * @throws ParseException on option parsing error. + */ + private ArgumentContext parseCommands(final File workingDirectory, final String[] args, + final Options options) throws IOException, ParseException { + CommandLine commandLine = parseCommandLine(options, args); ArgumentContext argumentContext = new ArgumentContext(workingDirectory, commandLine); - - if (commandLine.hasOption(HELP)) { - helpCmd.accept(opts); - return null; + Arg.processLogLevel(argumentContext, uiOptionCollection); + populateConfiguration(argumentContext); + if (uiOptionCollection.isSelected(Arg.HELP_LICENSES)) { + new Licenses(argumentContext.getConfiguration(), + new PrintWriter(argumentContext.getConfiguration().getOutput().get(), + false, StandardCharsets.UTF_8)).printHelp(); } - if (commandLine.hasOption(Arg.HELP_LICENSES.option())) { - new Licenses(createConfiguration(argumentContext), new PrintWriter(System.out, false, StandardCharsets.UTF_8)).printHelp(); - return null; - } - - ReportConfiguration configuration = createConfiguration(argumentContext); - if (!noArgs && !configuration.hasSource()) { - String msg = "No directories or files specified for scanning. Did you forget to close a multi-argument option?"; - DefaultLog.getInstance().error(msg); - helpCmd.accept(opts); - return null; - } - - return configuration; + return argumentContext; } /** @@ -171,38 +146,22 @@ public final class OptionCollection { * You probably want one of the {@code ParseCommands} methods. * @param argumentContext The context to execute in. * @return a ReportConfiguration - * @see #parseCommands(File, String[], Consumer) - * @see #parseCommands(File, String[], Consumer, boolean) */ - static ReportConfiguration createConfiguration(final ArgumentContext argumentContext) { - argumentContext.processArgs(); + private ReportConfiguration populateConfiguration(final ArgumentContext argumentContext) { + argumentContext.processArgs(uiOptionCollection); final ReportConfiguration configuration = argumentContext.getConfiguration(); final CommandLine commandLine = argumentContext.getCommandLine(); - if (Arg.DIR.isSelected()) { - try { - configuration.addSource(getReportable(commandLine.getParsedOptionValue(Arg.DIR.getSelected()), configuration)); - } catch (ParseException e) { - throw new ConfigurationException("Unable to set parse " + Arg.DIR.getSelected(), e); - } - } - for (String s : commandLine.getArgs()) { - IReportable reportable = getReportable(new File(s), configuration); - if (reportable != null) { - configuration.addSource(reportable); + if (!configuration.hasSource()) { + for (String s : commandLine.getArgs()) { + IReportable reportable = getReportable(new File(s), configuration); + if (reportable != null) { + configuration.addSource(reportable); + } } } return configuration; } - /** - * Create an {@code Options} object from the list of defined Options. - * Mutually exclusive options must be listed in an OptionGroup. - * @return the Options comprised of the Options defined in this class. - */ - public static Options buildOptions() { - return Arg.getOptions().addOption(HELP); - } - /** * Creates an IReportable object from the directory name and ReportConfiguration * object. @@ -211,7 +170,7 @@ public final class OptionCollection { * @param config the ReportConfiguration. * @return the IReportable instance containing the files. */ - static IReportable getReportable(final File base, final ReportConfiguration config) { + IReportable getReportable(final File base, final ReportConfiguration config) { File absBase = base.getAbsoluteFile(); DocumentName documentName = DocumentName.builder(absBase).build(); if (!absBase.exists()) { @@ -238,6 +197,7 @@ public final class OptionCollection { */ private static final class OptionComparator implements Comparator<Option>, Serializable { /** The serial version UID. */ + @Serial private static final long serialVersionUID = 5305467873966684014L; private String getKey(final Option opt) { @@ -301,7 +261,7 @@ public final class OptionCollection { /** * A style sheet. */ - STYLESHEET("StyleSheet", () -> format("Either an external xsl file or one of the internal named sheets. Internal sheets are: %n%s", + STYLESHEET("StyleSheet", () -> format("Either an external XSLT file or one of the internal named sheets. Internal sheets are: %n%s", Arrays.stream(StyleSheets.values()) .map(v -> format("\t%s: %s%n", v.arg(), v.desc())) .collect(Collectors.joining(System.lineSeparator())))), diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java index a9a2d5e7..c7888b47 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java +++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java @@ -21,15 +21,13 @@ package org.apache.rat.commandline; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.lang.reflect.Array; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Predicate; import org.apache.commons.cli.AlreadySelectedException; @@ -40,10 +38,11 @@ import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.io.IOUtils; -import org.apache.commons.io.function.IOSupplier; +import org.apache.commons.io.output.CloseShieldOutputStream; import org.apache.commons.lang3.tuple.Pair; import org.apache.rat.ConfigurationException; import org.apache.rat.Defaults; +import org.apache.rat.ImplementationException; import org.apache.rat.ReportConfiguration; import org.apache.rat.config.AddLicenseHeaders; import org.apache.rat.config.exclusion.ExclusionUtils; @@ -52,6 +51,7 @@ import org.apache.rat.document.DocumentName; import org.apache.rat.document.DocumentNameMatcher; import org.apache.rat.license.LicenseSetFactory; import org.apache.rat.report.claim.ClaimStatistic.Counter; +import org.apache.rat.ui.UIOptionCollection; import org.apache.rat.utils.DefaultLog; import org.apache.rat.utils.Log; @@ -64,7 +64,6 @@ import static java.lang.String.format; * This allows us to deprecate options as we move forward in development. */ public enum Arg { - ///////////////////////// EDIT OPTIONS /** * Defines options to add copyright to files @@ -79,7 +78,10 @@ public enum Arg { .addOption(Option.builder().longOpt("edit-copyright").hasArg() .desc("The copyright message to use in the license headers. Usually in the form of \"Copyright 2008 Foo\". " + "Only valid with --edit-license") - .build())), + .build()), + (context, selected) -> { + throw new ImplementationException(String.format("'%s' should not be executed directly", selected)); + }), /** * Causes file updates to overwrite existing files. @@ -93,7 +95,11 @@ public enum Arg { .addOption(Option.builder().longOpt("edit-overwrite") .desc("Forces any changes in files to be written directly to the source files so that new files are not created. " + "Only valid with --edit-license.") - .build())), + .build()), + (context, selected) -> { + throw new ImplementationException(String.format("'%s' should not be executed directly", selected)); + } + ), /** * Defines options to add licenses to files @@ -112,7 +118,11 @@ public enum Arg { "Add the Apache-2.0 license header to any file with an unknown license that is not in the exclusion list. " + "By default new files will be created with the license header, " + "to force the modification of existing files use the --edit-overwrite option.").build() - )), + ), + (context, selected) -> { + throw new ImplementationException(String.format("'%s' should not be executed directly", selected)); + } + ), //////////////////////////// CONFIGURATION OPTIONS /** @@ -129,7 +139,11 @@ public enum Arg { .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--config")).get()) .converter(Converters.FILE_CONVERTER) .type(File.class) - .build())), + .build()), + (context, selected) -> { + throw new ImplementationException(String.format("'%s' should not be executed directly", selected)); + } + ), /** * Group of options that skip the default configuration file @@ -143,16 +157,22 @@ public enum Arg { .setForRemoval(true) .setDescription(StdMsgs.useMsg("--configuration-no-defaults")).get()) .desc("Ignore default configuration.") - .build())), + .build()), + (context, selected) -> { + throw new ImplementationException(String.format("'%s' should not be executed directly", selected)); + }), /** * Option that adds approved licenses to the list */ LICENSES_APPROVED(new OptionGroup().addOption(Option.builder().longOpt("licenses-approved").hasArg().argName("LicenseID") .desc("A comma separated list of approved License IDs. These licenses will be added to the list of approved licenses.") - .converter(Converters.TEXT_LIST_CONVERTER) - .type(String[].class) - .build())), + .converter(Converters.TEXT_LIST_CONVERTER) + .type(String[].class) + .build()), + (context, selected) -> + context.getConfiguration().addApprovedLicenseIds(processArrayArg(context, selected)) + ), /** * Option that adds approved licenses from a file @@ -161,7 +181,9 @@ public enum Arg { .desc("Name of file containing comma separated lists of approved License IDs.") .converter(Converters.FILE_CONVERTER) .type(File.class) - .build())), + .build()), + (context, selected) -> + context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, selected))), /** * Option that specifies approved license families @@ -170,7 +192,8 @@ public enum Arg { .desc("A comma separated list of approved license family IDs. These license families will be added to the list of approved license families.") .converter(Converters.TEXT_LIST_CONVERTER) .type(String[].class) - .build())), + .build()), + (context, selected) -> context.getConfiguration().addApprovedLicenseCategories(processArrayArg(context, selected))), /** * Option that specifies approved license families from a file @@ -179,7 +202,9 @@ public enum Arg { .desc("Name of file containing comma separated lists of approved family IDs.") .converter(Converters.FILE_CONVERTER) .type(File.class) - .build())), + .build()), + (context, selected) -> context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, selected)) + ), /** * Option to remove licenses from the approved list @@ -190,7 +215,8 @@ public enum Arg { "Once licenses are removed they can not be added back.") .converter(Converters.TEXT_LIST_CONVERTER) .type(String[].class) - .build())), + .build()), + (context, selected) -> context.getConfiguration().removeApprovedLicenseIds(processArrayArg(context, selected))), /** * Option to read a file licenses to be removed from the approved list. @@ -201,7 +227,8 @@ public enum Arg { .desc("Name of file containing comma separated lists of the denied license IDs. " + "These licenses will be removed from the list of approved licenses. " + "Once licenses are removed they can not be added back.") - .build())), + .build()), + (context, selected) -> context.getConfiguration().removeApprovedLicenseIds(processArrayFile(context, selected))), /** * Option to list license families to remove from the approved list. @@ -213,7 +240,8 @@ public enum Arg { "Once license families are removed they can not be added back.") .converter(Converters.TEXT_LIST_CONVERTER) .type(String[].class) - .build())), + .build()), + (context, selected) -> context.getConfiguration().removeApprovedLicenseCategories(processArrayArg(context, selected))), /** * Option to read a list of license families to remove from the approved list. @@ -224,7 +252,8 @@ public enum Arg { "Once license families are removed they can not be added back.") .type(File.class) .converter(Converters.FILE_CONVERTER) - .build())), + .build()), + (context, selected) -> context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, selected))), /** * Option to specify an acceptable number of various counters. @@ -233,7 +262,14 @@ public enum Arg { .desc("The acceptable maximum number for the specified counter. A value of '-1' specifies an unlimited number.") .converter(Converters.COUNTER_CONVERTER) .type(Pair.class) - .build())), + .build()), + (context, selected) -> { + for (String arg : context.getCommandLine().getOptionValues(selected)) { + Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg); + int limit = pair.getValue(); + context.getConfiguration().getClaimValidator().setMax(pair.getKey(), limit < 0 ? Integer.MAX_VALUE : limit); + } + }), /** * Option to specify an acceptable number of various counters. @@ -242,7 +278,13 @@ public enum Arg { .desc("The minimum number for the specified counter.") .converter(Converters.COUNTER_CONVERTER) .type(Pair.class) - .build())), + .build()), + (context, selected) -> { + for (String arg : context.getCommandLine().getOptionValues(selected)) { + Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg); + context.getConfiguration().getClaimValidator().setMin(pair.getKey(), pair.getValue()); + } + }), ////////////////// INPUT OPTIONS /** @@ -256,7 +298,13 @@ public enum Arg { "argument is located.") .converter(Converters.FILE_CONVERTER) .type(File.class) - .build())), + .build()), + (context, selected) -> { + File[] files = getParsedOptionValues(selected, context.getCommandLine()); + for (File f : files) { + context.getConfiguration().addSource(f); + } + }), /** * Excludes files by expression @@ -269,7 +317,13 @@ public enum Arg { .build()) .addOption(Option.builder().longOpt("input-exclude").hasArgs().argName("Expression") .desc("Excludes files matching <Expression>.") - .build())), + .build()), + (context, selected) -> { + String[] excludes = context.getCommandLine().getOptionValues(selected); + if (excludes != null) { + context.getConfiguration().addExcludedPatterns(Arrays.asList(excludes)); + } + }), /** * Excludes files based on the contents of a file. @@ -286,7 +340,17 @@ public enum Arg { .argName("File").hasArg().type(File.class) .converter(Converters.FILE_CONVERTER) .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.") - .build())), + .build()), + (context, selected) -> { + try { + File excludeFileName = context.getCommandLine().getParsedOptionValue(selected); + if (excludeFileName != null) { + context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName, "#")); + } + } catch (Exception e) { + throw ConfigurationException.from(e); + } + }), /** * Excludes files based on standard groupings. */ @@ -296,8 +360,12 @@ public enum Arg { .desc("Excludes files defined in standard collections based on commonly occurring groups. " + "Excludes any path matcher actions but DOES NOT exclude any file processor actions.") .type(StandardCollection.class) - .build()) - ), + .build()), + (context, selected) -> { + for (String s : context.getCommandLine().getOptionValues(selected)) { + context.getConfiguration().addExcludedCollection(StandardCollection.valueOf(s)); + } + }), /** * Excludes files if they are smaller than the given threshold. @@ -306,8 +374,20 @@ public enum Arg { .addOption(Option.builder().longOpt("input-exclude-size").argName("Integer") .hasArg().type(Integer.class) .desc("Excludes files with sizes less than the number of bytes specified.") - .build()) - ), + .build()), + (context, selected) -> { + try { + final int maxSize = context.getCommandLine().getParsedOptionValue(selected); + DocumentNameMatcher matcher = new DocumentNameMatcher(String.format("File size < %s bytes", maxSize), + (Predicate<DocumentName>) documentName -> { + File f = new File(documentName.getName()); + return f.isFile() && f.length() < maxSize; + }); + context.getConfiguration().addExcludedMatcher(matcher); + } catch (Exception e) { + throw ConfigurationException.from(e); + } + }), /** * Excludes files by expression. */ @@ -319,8 +399,13 @@ public enum Arg { .desc("Includes files matching <Expression>. Will override excluded files.") .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17") .setDescription(StdMsgs.useMsg("--input-include")).get()) - .build()) - ), + .build()), + (context, selected) -> { + String[] includes = context.getCommandLine().getOptionValues(selected); + if (includes != null) { + context.getConfiguration().addIncludedPatterns(Arrays.asList(includes)); + } + }), /** * Includes files based on the contents of a file. @@ -337,7 +422,17 @@ public enum Arg { .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.") .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17") .setDescription(StdMsgs.useMsg("--input-include-file")).get()) - .build())), + .build()), + (context, selected) -> { + try { + File includeFileName = context.getCommandLine().getParsedOptionValue(selected); + if (includeFileName != null) { + context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName, "#")); + } + } catch (Exception e) { + throw ConfigurationException.from(e); + } + }), /** * Includes files based on standard groups. @@ -353,8 +448,17 @@ public enum Arg { .desc("Scans hidden directories.") .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17") .setDescription(StdMsgs.useMsg("--input-include-std with 'HIDDEN_DIR' argument")).get()).build() - ) - ), + ), + (context, selected) -> { + // display deprecation log if needed. + if (context.getCommandLine().hasOption("scan-hidden-directories")) { + context.getConfiguration().addIncludedCollection(StandardCollection.HIDDEN_DIR); + } else { + for (String s : context.getCommandLine().getOptionValues(selected)) { + context.getConfiguration().addIncludedCollection(StandardCollection.valueOf(s)); + } + } + }), /** * Excludes files based on SCM exclusion file processing. @@ -366,17 +470,32 @@ public enum Arg { .desc("Parse SCM based exclusion files to exclude specified files and directories. " + "This action can apply to any standard collection that implements a file processor.") .type(StandardCollection.class) - .build()) - ), + .build()), + (context, selected) -> { + StandardCollection[] collections = getParsedOptionValues(selected, context.getCommandLine()); + final ReportConfiguration configuration = context.getConfiguration(); + for (StandardCollection collection : collections) { + if (collection == StandardCollection.ALL) { + Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedFileProcessor); + Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedCollection); + } else { + configuration.addExcludedFileProcessor(collection); + configuration.addExcludedCollection(collection); + } + } + }), /** * Stop processing an input stream and declare an input file. */ DIR(new OptionGroup().addOption(Option.builder().option("d").longOpt("dir").hasArg() - .type(File.class) + .type(File.class) .desc("Used to indicate end of list when using options that take multiple arguments.").argName("DirOrArchive") .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17") - .setDescription("Use the standard '--' to signal the end of arguments.").get()).build())), + .setDescription("Use the standard '--' to signal the end of arguments.").get()).build()), + (context, selected) -> { + throw new ImplementationException(String.format("'%s' should not be executed directly", selected)); + }), /////////////// OUTPUT OPTIONS /** @@ -397,7 +516,22 @@ public enum Arg { .setForRemoval(true) .setDescription(StdMsgs.useMsg("--output-style with the 'xml' argument")).get()) .desc("forces XML output rather than the textual report.") - .build())), + .build()), + (context, selected) -> { + String key = selected.getKey(); // is not null due to above isSelected()-call + if ("x".equals(key)) { + // display deprecated message. + context.getCommandLine().hasOption("x"); + context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml")); + } else { + String[] style = context.getCommandLine().getOptionValues(selected); + if (style.length != 1) { + DefaultLog.getInstance().error("Please specify a single stylesheet"); + throw new ConfigurationException("Please specify a single stylesheet"); + } + context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0])); + } + }), /** * Specifies the license definitions that should be included in the output. @@ -411,7 +545,14 @@ public enum Arg { .desc("List the defined licenses.") .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase())) .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-licenses")).get()) - .build())), + .build()), + (context, selected) -> { + try { + context.getConfiguration().listLicenses(context.getCommandLine().getParsedOptionValue(selected)); + } catch (ParseException e) { + context.logParseException(e, selected, Defaults.LIST_LICENSES); + } + }), /** * Specifies the license families that should be included in the output. @@ -425,7 +566,14 @@ public enum Arg { .desc("List the defined license families.") .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase())) .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-families")).get()) - .build())), + .build()), + (context, selected) -> { + try { + context.getConfiguration().listFamilies(context.getCommandLine().getParsedOptionValue(selected)); + } catch (ParseException e) { + context.logParseException(e, selected, Defaults.LIST_FAMILIES); + } + }), /** * Specifies the log level to log messages at. @@ -434,14 +582,25 @@ public enum Arg { .hasArg().argName("LogLevel") .desc("Sets the log level.") .converter(s -> Log.Level.valueOf(s.toUpperCase())) - .build())), + .build()), + (context, selected) -> { + Log dLog = DefaultLog.getInstance(); + try { + dLog.setLevel(context.getCommandLine().getParsedOptionValue(selected)); + } catch (ParseException e) { + logParseException(DefaultLog.getInstance(), e, selected, context.getCommandLine(), dLog.getLevel()); + } + }), /** * Specifies that the run should not perform any updates to files. */ DRY_RUN(new OptionGroup().addOption(Option.builder().longOpt("dry-run") .desc("If set do not update the files but generate the reports.") - .build())), + .build()), + (context, selected) -> + context.getConfiguration().setDryRun(true) + ), /** * Specifies where the output should be written. @@ -457,7 +616,20 @@ public enum Arg { .desc("Define the output file where to write a report to.") .type(File.class) .converter(Converters.FILE_CONVERTER) - .build())), + .build()), + (context, selected) -> { + try { + File file = context.getCommandLine().getParsedOptionValue(selected); + File parent = file.getParentFile(); + if (!parent.mkdirs() && !parent.isDirectory()) { + DefaultLog.getInstance().error("Could not create report parent directory " + file); + } + context.getConfiguration().setOut(file); + } catch (ParseException e) { + context.logParseException(e, selected, "System.out"); + context.getConfiguration().setOut(() -> CloseShieldOutputStream.wrap(System.out)); + } + }), /** * Specifies the level of reporting detail for archive files. @@ -466,7 +638,15 @@ public enum Arg { .addOption(Option.builder().longOpt("output-archive").hasArg().argName("ProcessingType") .desc("Specifies the level of detail in ARCHIVE file reporting.") .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase())) - .build())), + .build()), + (context, selected) -> { + try { + context.getConfiguration().setArchiveProcessing(context.getCommandLine().getParsedOptionValue(selected)); + } catch (ParseException e) { + context.logParseException(e, selected, Defaults.ARCHIVE_PROCESSING); + } + } + ), /** * Specifies the level of reporting detail for standard files. @@ -475,51 +655,57 @@ public enum Arg { .addOption(Option.builder().longOpt("output-standard").hasArg().argName("ProcessingType") .desc("Specifies the level of detail in STANDARD file reporting.") .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase())) - .build())), + .build()), + (context, selected) -> { + try { + context.getConfiguration().setStandardProcessing(context.getCommandLine().getParsedOptionValue(selected)); + } catch (ParseException e) { + context.logParseException(e, selected, Defaults.STANDARD_PROCESSING); + } + }), /** * Provide license definition listing of registered licenses. */ HELP_LICENSES(new OptionGroup() .addOption(Option.builder().longOpt("help-licenses") // - .desc("Print information about registered licenses.").build())); + .desc("Print information about registered licenses.").build()), + (context, selected) -> { + throw new ImplementationException(String.format("'%s' should not be executed directly", selected)); + }); - /** The option group for the argument */ + /** + * The option group for the argument + */ private final OptionGroup group; + /** + * The apply the option to update the state of the context.configuration. + */ + private final BiConsumer<ArgumentContext, Option> process; + /** * Creates an Arg from an Option group. * * @param group The option group. */ - Arg(final OptionGroup group) { + Arg(final OptionGroup group, final BiConsumer<ArgumentContext, Option> process) { this.group = group; + this.process = process; } - /** - * Determines if the group has a selected element. - * - * @return {@code true} if the group has a selected element. - */ - public boolean isSelected() { - return group.getSelected() != null; + private void execute(final ArgumentContext context, final UIOptionCollection<?> optionCollection) { + optionCollection.getSelected(this) + .ifPresent(selected -> this.process.accept(context, selected)); } /** - * Gets the select element from the group. + * Determines if all the options have been removed from this argument. * - * @return the selected element or {@code null} if no element is selected. - */ - public Option getSelected() { - String s = group.getSelected(); - if (s != null) { - for (Option result : group.getOptions()) { - if (result.getKey().equals(s)) { - return result; - } - } - } - return null; + * @return {@code true} if all the options have been removed from this argument. + */ + public boolean isEmpty() { + return this.group().getOptions().isEmpty(); } /** @@ -538,14 +724,6 @@ public enum Arg { throw new IllegalArgumentException("Can not find " + key); } - /** - * Gets the default value for this arg. - * @return default value of this arg. - */ - public String defaultValue() { - return DEFAULT_VALUES.get(this); - } - /** * Gets the group for this arg. * @@ -558,20 +736,25 @@ public enum Arg { /** * Returns the first non-deprecated option from the group. * - * @return the first non-deprecated option or {@code null} if no non-deprecated option is available. + * @return the first non-deprecated option or, if no non-deprecated option is available, the first option. */ public Option option() { + Option first = null; for (Option result : group.getOptions()) { + if (first == null) { + first = result; + } if (!result.isDeprecated()) { return result; } } - return null; + return first; } /** * Gets the full set of options. - * @return the full set of options for this Arg. + * + * @return the full set of options for this Arg. */ public static Options getOptions() { Options options = new Options(); @@ -586,36 +769,44 @@ public enum Arg { * * @param context the context to work with. */ - private static void processEditArgs(final ArgumentContext context) { - if (EDIT_ADD.isSelected()) { - context.getCommandLine().hasOption(Arg.EDIT_ADD.getSelected()); - boolean force = EDIT_OVERWRITE.isSelected(); + private static void processEditArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) { + optionCollection.getSelected(EDIT_ADD).ifPresent(option -> { + // prints deprecation + context.getCommandLine().hasOption(option); + boolean force = optionCollection.isSelected(EDIT_OVERWRITE); if (force) { - context.getCommandLine().hasOption(EDIT_OVERWRITE.getSelected()); + // prints deprecation + optionCollection.getSelected(EDIT_OVERWRITE).ifPresent(context.getCommandLine()::hasOption); } context.getConfiguration().setAddLicenseHeaders(force ? AddLicenseHeaders.FORCED : AddLicenseHeaders.TRUE); - if (EDIT_COPYRIGHT.isSelected()) { - context.getConfiguration().setCopyrightMessage(context.getCommandLine().getOptionValue(EDIT_COPYRIGHT.getSelected())); - } - } + optionCollection.getSelected(EDIT_COPYRIGHT). + ifPresent(editOption -> context.getConfiguration().setCopyrightMessage(context.getCommandLine().getOptionValue(editOption))); + }); } - private static List<String> processArrayArg(final ArgumentContext context, final Arg arg) throws ParseException { - String[] ids = context.getCommandLine().getParsedOptionValue(arg.getSelected()); - return Arrays.asList(ids); + private static List<String> processArrayArg(final ArgumentContext context, final Option selected) { + try { + return Arrays.asList(context.getCommandLine().getParsedOptionValue(selected)); + } catch (ParseException e) { + throw new ConfigurationException(e); + } } - private static List<String> processArrayFile(final ArgumentContext context, final Arg arg) throws ParseException { + private static List<String> processArrayFile(final ArgumentContext context, final Option selected) { List<String> result = new ArrayList<>(); - File file = context.getCommandLine().getParsedOptionValue(arg.getSelected()); - try (InputStream in = Files.newInputStream(file.toPath())) { - for (String line : IOUtils.readLines(in, StandardCharsets.UTF_8)) { - String[] ids = Converters.TEXT_LIST_CONVERTER.apply(line); - result.addAll(Arrays.asList(ids)); + try { + File file = context.getCommandLine().getParsedOptionValue(selected); + try (InputStream in = Files.newInputStream(file.toPath())) { + for (String line : IOUtils.readLines(in, StandardCharsets.UTF_8)) { + String[] ids = Converters.TEXT_LIST_CONVERTER.apply(line); + result.addAll(Arrays.asList(ids)); + } + return result; + } catch (IOException e) { + throw new ConfigurationException(e); } - return result; - } catch (IOException e) { - throw new ConfigurationException(e); + } catch (ParseException e) { + throw ConfigurationException.from(e); } } @@ -625,64 +816,28 @@ public enum Arg { * @param context the context to process. * @throws ConfigurationException if configuration files can not be read. */ - private static void processConfigurationArgs(final ArgumentContext context) throws ConfigurationException { - try { - Defaults.Builder defaultBuilder = Defaults.builder(); - if (CONFIGURATION.isSelected()) { - File[] files = CONFIGURATION.getParsedOptionValues(context.getCommandLine()); - for (File file : files) { - defaultBuilder.add(file); - } - } - if (CONFIGURATION_NO_DEFAULTS.isSelected()) { - // display deprecation log if needed. - context.getCommandLine().hasOption(CONFIGURATION_NO_DEFAULTS.getSelected()); - defaultBuilder.noDefault(); - } - context.getConfiguration().setFrom(defaultBuilder.build()); - - if (FAMILIES_APPROVED.isSelected()) { - context.getConfiguration().addApprovedLicenseCategories(processArrayArg(context, FAMILIES_APPROVED)); - } - if (FAMILIES_APPROVED_FILE.isSelected()) { - context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, FAMILIES_APPROVED_FILE)); - } - if (FAMILIES_DENIED.isSelected()) { - context.getConfiguration().removeApprovedLicenseCategories(processArrayArg(context, FAMILIES_DENIED)); - } - if (FAMILIES_DENIED_FILE.isSelected()) { - context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, FAMILIES_DENIED_FILE)); - } - - if (LICENSES_APPROVED.isSelected()) { - context.getConfiguration().addApprovedLicenseIds(processArrayArg(context, LICENSES_APPROVED)); - } + private static void processConfigurationArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException { - if (LICENSES_APPROVED_FILE.isSelected()) { - context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, LICENSES_APPROVED_FILE)); - } - if (LICENSES_DENIED.isSelected()) { - context.getConfiguration().removeApprovedLicenseIds(processArrayArg(context, LICENSES_DENIED)); - } + Defaults.Builder defaultBuilder = Defaults.builder(); - if (LICENSES_DENIED_FILE.isSelected()) { - context.getConfiguration().removeApprovedLicenseIds(processArrayFile(context, LICENSES_DENIED_FILE)); - } - if (COUNTER_MAX.isSelected()) { - for (String arg : context.getCommandLine().getOptionValues(COUNTER_MAX.getSelected())) { - Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg); - int limit = pair.getValue(); - context.getConfiguration().getClaimValidator().setMax(pair.getKey(), limit < 0 ? Integer.MAX_VALUE : limit); - } - } - if (COUNTER_MIN.isSelected()) { - for (String arg : context.getCommandLine().getOptionValues(COUNTER_MIN.getSelected())) { - Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg); - context.getConfiguration().getClaimValidator().setMin(pair.getKey(), pair.getValue()); - } - } - } catch (Exception e) { - throw ConfigurationException.from(e); + optionCollection.getSelected(CONFIGURATION).ifPresent( + selected -> { + File[] files = getParsedOptionValues(selected, context.getCommandLine()); + for (File file : files) { + defaultBuilder.add(file); + } + }); + optionCollection.getSelected(CONFIGURATION_NO_DEFAULTS).ifPresent(selected -> { + // display deprecation log if needed. + context.getCommandLine().hasOption(selected); + defaultBuilder.noDefault(); + }); + context.getConfiguration().setFrom(defaultBuilder.build()); + + for (Arg arg : new Arg[]{FAMILIES_APPROVED, FAMILIES_APPROVED_FILE, FAMILIES_DENIED, FAMILIES_DENIED_FILE, + LICENSES_APPROVED, LICENSES_APPROVED_FILE, LICENSES_DENIED, LICENSES_DENIED_FILE, + COUNTER_MAX, COUNTER_MIN}) { + arg.execute(context, optionCollection); } } @@ -692,91 +847,21 @@ public enum Arg { * @param context the context to work in. * @throws ConfigurationException if an exclude file can not be read. */ - private static void processInputArgs(final ArgumentContext context) throws ConfigurationException { - try { - if (SOURCE.isSelected()) { - File[] files = SOURCE.getParsedOptionValues(context.getCommandLine()); - for (File f : files) { - context.getConfiguration().addSource(f); - } - } - // TODO when include/exclude processing is updated check calling methods to ensure that all specified - // directories are handled in the list of directories. - if (EXCLUDE.isSelected()) { - String[] excludes = context.getCommandLine().getOptionValues(EXCLUDE.getSelected()); - if (excludes != null) { - context.getConfiguration().addExcludedPatterns(Arrays.asList(excludes)); - } - } - if (EXCLUDE_FILE.isSelected()) { - File excludeFileName = context.getCommandLine().getParsedOptionValue(EXCLUDE_FILE.getSelected()); - if (excludeFileName != null) { - context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName, "#")); - } - } - if (EXCLUDE_STD.isSelected()) { - for (String s : context.getCommandLine().getOptionValues(EXCLUDE_STD.getSelected())) { - context.getConfiguration().addExcludedCollection(StandardCollection.valueOf(s)); - } - } - if (EXCLUDE_PARSE_SCM.isSelected()) { - StandardCollection[] collections = EXCLUDE_PARSE_SCM.getParsedOptionValues(context.getCommandLine()); - final ReportConfiguration configuration = context.getConfiguration(); - for (StandardCollection collection : collections) { - if (collection == StandardCollection.ALL) { - Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedFileProcessor); - Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedCollection); - } else { - configuration.addExcludedFileProcessor(collection); - configuration.addExcludedCollection(collection); - } - } - } - if (EXCLUDE_SIZE.isSelected()) { - final int maxSize = EXCLUDE_SIZE.getParsedOptionValue(context.getCommandLine()); - DocumentNameMatcher matcher = new DocumentNameMatcher(String.format("File size < %s bytes", maxSize), - (Predicate<DocumentName>) documentName -> { - File f = new File(documentName.getName()); - return f.isFile() && f.length() < maxSize; - }); - context.getConfiguration().addExcludedMatcher(matcher); - } - if (INCLUDE.isSelected()) { - String[] includes = context.getCommandLine().getOptionValues(INCLUDE.getSelected()); - if (includes != null) { - context.getConfiguration().addIncludedPatterns(Arrays.asList(includes)); - } - } - if (INCLUDE_FILE.isSelected()) { - File includeFileName = context.getCommandLine().getParsedOptionValue(INCLUDE_FILE.getSelected()); - if (includeFileName != null) { - context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName, "#")); - } - } - if (INCLUDE_STD.isSelected()) { - Option selected = INCLUDE_STD.getSelected(); - // display deprecation log if needed. - if (context.getCommandLine().hasOption("scan-hidden-directories")) { - context.getConfiguration().addIncludedCollection(StandardCollection.HIDDEN_DIR); - } else { - for (String s : context.getCommandLine().getOptionValues(selected)) { - context.getConfiguration().addIncludedCollection(StandardCollection.valueOf(s)); - } - } - } - } catch (Exception e) { - throw ConfigurationException.from(e); + private static void processInputArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException { + for (Arg arg : new Arg[]{SOURCE, EXCLUDE, EXCLUDE_FILE, EXCLUDE_STD, EXCLUDE_PARSE_SCM, EXCLUDE_SIZE, + INCLUDE, INCLUDE_FILE, INCLUDE_STD}) { + arg.execute(context, optionCollection); } } /** * Logs a ParseException as a warning. * - * @param log the Log to write to + * @param log the Log to write to * @param exception the parse exception to log - * @param opt the option being processed - * @param cl the command line being processed - * @param defaultValue The default value the option is being set to. + * @param opt the option being processed + * @param cl the command line being processed + * @param defaultValue The default value the option is being set to. */ private static void logParseException(final Log log, final ParseException exception, final Option opt, final CommandLine cl, final Object defaultValue) { log.warn(format("Invalid %s specified: %s ", opt.getOpt(), cl.getOptionValue(opt))); @@ -787,17 +872,10 @@ public enum Arg { /** * Process the log level setting. * - * @param commandLine The command line to process. + * @param context The argument context */ - public static void processLogLevel(final CommandLine commandLine) { - if (LOG_LEVEL.getSelected() != null) { - Log dLog = DefaultLog.getInstance(); - try { - dLog.setLevel(commandLine.getParsedOptionValue(LOG_LEVEL.getSelected())); - } catch (ParseException e) { - logParseException(DefaultLog.getInstance(), e, LOG_LEVEL.getSelected(), commandLine, dLog.getLevel()); - } - } + public static void processLogLevel(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException { + LOG_LEVEL.execute(context, optionCollection); } /** @@ -806,12 +884,12 @@ public enum Arg { * @param context the context in which to process the args. * @throws ConfigurationException on error */ - public static void processArgs(final ArgumentContext context) throws ConfigurationException { + public static void processArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException { Converters.FILE_CONVERTER.setWorkingDirectory(context.getWorkingDirectory()); - processOutputArgs(context); - processEditArgs(context); - processInputArgs(context); - processConfigurationArgs(context); + processOutputArgs(context, optionCollection); + processEditArgs(context, optionCollection); + processInputArgs(context, optionCollection); + processConfigurationArgs(context, optionCollection); } /** @@ -819,69 +897,9 @@ public enum Arg { * * @param context the context in which to process the args. */ - private static void processOutputArgs(final ArgumentContext context) { - context.getConfiguration().setDryRun(DRY_RUN.isSelected()); - - if (OUTPUT_FAMILIES.isSelected()) { - try { - context.getConfiguration().listFamilies(context.getCommandLine().getParsedOptionValue(OUTPUT_FAMILIES.getSelected())); - } catch (ParseException e) { - context.logParseException(e, OUTPUT_FAMILIES.getSelected(), Defaults.LIST_FAMILIES); - } - } - - if (OUTPUT_LICENSES.isSelected()) { - try { - context.getConfiguration().listLicenses(context.getCommandLine().getParsedOptionValue(OUTPUT_LICENSES.getSelected())); - } catch (ParseException e) { - context.logParseException(e, OUTPUT_LICENSES.getSelected(), Defaults.LIST_LICENSES); - } - } - - if (OUTPUT_ARCHIVE.isSelected()) { - try { - context.getConfiguration().setArchiveProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_ARCHIVE.getSelected())); - } catch (ParseException e) { - context.logParseException(e, OUTPUT_ARCHIVE.getSelected(), Defaults.ARCHIVE_PROCESSING); - } - } - - if (OUTPUT_STANDARD.isSelected()) { - try { - context.getConfiguration().setStandardProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_STANDARD.getSelected())); - } catch (ParseException e) { - context.logParseException(e, OUTPUT_STANDARD.getSelected(), Defaults.STANDARD_PROCESSING); - } - } - - if (OUTPUT_FILE.isSelected()) { - try { - File file = context.getCommandLine().getParsedOptionValue(OUTPUT_FILE.getSelected()); - File parent = file.getParentFile(); - if (!parent.mkdirs() && !parent.isDirectory()) { - DefaultLog.getInstance().error("Could not create report parent directory " + file); - } - context.getConfiguration().setOut(file); - } catch (ParseException e) { - context.logParseException(e, OUTPUT_FILE.getSelected(), "System.out"); - context.getConfiguration().setOut((IOSupplier<OutputStream>) null); - } - } - - if (OUTPUT_STYLE.isSelected()) { - String selected = OUTPUT_STYLE.getSelected().getKey(); // is not null due to above isSelected()-call - if ("x".equals(selected)) { - // display deprecated message. - context.getCommandLine().hasOption("x"); - context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml")); - } else { - String[] style = context.getCommandLine().getOptionValues(OUTPUT_STYLE.getSelected()); - if (style.length != 1) { - DefaultLog.getInstance().error("Please specify a single stylesheet"); - throw new ConfigurationException("Please specify a single stylesheet"); - } - context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0])); - } + private static void processOutputArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException { + for (Arg arg : new Arg[]{DRY_RUN, OUTPUT_FAMILIES, OUTPUT_LICENSES, OUTPUT_ARCHIVE, OUTPUT_STANDARD, OUTPUT_FILE, OUTPUT_STYLE}) { + arg.execute(context, optionCollection); } } @@ -898,27 +916,9 @@ public enum Arg { } } - /** - * Finds the Arg that an Option is in. - * - * @param optionToFind the Option to locate. - * @return The Arg or {@code null} if no Arg is found. - */ - public static Arg findArg(final Option optionToFind) { - if (optionToFind != null) { - for (Arg arg : Arg.values()) { - for (Option candidate : arg.group.getOptions()) { - if (optionToFind.equals(candidate)) { - return arg; - } - } - } - } - return null; - } - /** * Finds the Arg that contains an Option with the specified key. + * * @param key the key for the Option to locate. * @return The Arg or {@code null} if no Arg is found. */ @@ -935,31 +935,18 @@ public enum Arg { return null; } - private <T> T getParsedOptionValue(final CommandLine commandLine) throws ParseException { - return commandLine.getParsedOptionValue(this.getSelected()); - } - - private String getOptionValue(final CommandLine commandLine) { - return commandLine.getOptionValue(this.getSelected()); - } - - private String[] getOptionValues(final CommandLine commandLine) { - return commandLine.getOptionValues(this.getSelected()); - } - - private <T> T[] getParsedOptionValues(final CommandLine commandLine) { - Option option = getSelected(); + private static <T> T[] getParsedOptionValues(final Option selected, final CommandLine commandLine) { try { - Class<? extends T> clazz = (Class<? extends T>) option.getType(); - String[] values = commandLine.getOptionValues(option); + Class<? extends T> clazz = (Class<? extends T>) selected.getType(); + String[] values = commandLine.getOptionValues(selected); T[] result = (T[]) Array.newInstance(clazz, values.length); for (int i = 0; i < values.length; i++) { - result[i] = clazz.cast(option.getConverter().apply(values[i])); + result[i] = clazz.cast(selected.getConverter().apply(values[i])); } return result; } catch (Throwable t) { - throw new ConfigurationException(format("'%s' converter for %s '%s' does not produce a class of type %s", this, - option.getKey(), option.getConverter().getClass().getName(), option.getType()), t); + throw new ConfigurationException(format("'%s' converter for %s '%s' does not produce a class of type %s", selected, + selected.getKey(), selected.getConverter().getClass().getName(), selected.getType()), t); } } @@ -981,18 +968,4 @@ public enum Arg { return format("Use %s instead.", name); } } - - /** - * The default values description map - */ - private static final Map<Arg, String> DEFAULT_VALUES = new HashMap<>(); - - static { - DEFAULT_VALUES.put(OUTPUT_FILE, "System.out"); - DEFAULT_VALUES.put(LOG_LEVEL, Log.Level.WARN.name()); - DEFAULT_VALUES.put(OUTPUT_ARCHIVE, Defaults.ARCHIVE_PROCESSING.name()); - DEFAULT_VALUES.put(OUTPUT_STANDARD, Defaults.STANDARD_PROCESSING.name()); - DEFAULT_VALUES.put(OUTPUT_LICENSES, Defaults.LIST_LICENSES.name()); - DEFAULT_VALUES.put(OUTPUT_FAMILIES, Defaults.LIST_FAMILIES.name()); - } } diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java index ea672bc8..d4374a57 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java +++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java @@ -25,6 +25,7 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.ParseException; import org.apache.rat.ReportConfiguration; import org.apache.rat.document.DocumentName; +import org.apache.rat.ui.UIOptionCollection; import org.apache.rat.utils.DefaultLog; import static java.lang.String.format; @@ -33,7 +34,7 @@ import static java.lang.String.format; * Provides the context necessary to process various arguments. * @since 0.17 */ -public class ArgumentContext { +public final class ArgumentContext { /** The report configuration that is being built */ private final ReportConfiguration configuration; /** The command line that is building the configuration */ @@ -65,8 +66,8 @@ public class ArgumentContext { /** * Process the arguments specified in this context. */ - public void processArgs() { - Arg.processArgs(this); + public void processArgs(final UIOptionCollection<?> uiOptionCollection) { + Arg.processArgs(this, uiOptionCollection); } /** diff --git a/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java b/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java index eff2b49a..e7a8e336 100644 --- a/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java +++ b/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java @@ -30,9 +30,9 @@ import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.WordUtils; +import org.apache.rat.CLIOptionCollection; import org.apache.rat.OptionCollection; import org.apache.rat.VersionInfo; -import org.apache.rat.commandline.Arg; import static java.lang.String.format; @@ -185,8 +185,7 @@ public abstract class AbstractHelp { optBuf.append(END_OF_OPTION_MSG); } // check for default value - Arg arg = Arg.findArg(option); - String defaultValue = arg == null ? null : arg.defaultValue(); + String defaultValue = CLIOptionCollection.INSTANCE.defaultValue(option); if (defaultValue != null) { optBuf.append(format(" (Default value = %s)", defaultValue)); } diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/AbstractCodeGenerator.java b/apache-rat-core/src/main/java/org/apache/rat/ui/AbstractCodeGenerator.java new file mode 100644 index 00000000..3eb5cdae --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/AbstractCodeGenerator.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui; + +import java.io.IOException; +import java.util.function.Function; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.text.WordUtils; +import org.apache.rat.DeprecationReporter; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.runtime.RuntimeConstants; + +import static java.lang.String.format; +import static org.apache.rat.OptionCollectionParser.ArgumentType.NONE; + +/** + * Generates the ${code org.apache.rat.maven.AbstractMaven} source code. + * @param <T> The concrete implementation of the AbstractOption. + */ +public abstract class AbstractCodeGenerator<T extends UIOption<?>> { + /** The base source directory */ + protected final String baseDirectory; + /** the velocity engine to generate files with */ + protected final VelocityEngine velocityEngine; + /** + * private constructor. + * @param baseDirectory The base source directory. + */ + protected AbstractCodeGenerator(final String baseDirectory) { + this.baseDirectory = baseDirectory; + velocityEngine = new VelocityEngine(); + velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); + velocityEngine.setProperty("classpath.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + velocityEngine.init(); + } + + /** + * Gets the options for the command line. + * @return the command line options. + */ + private static Options getOptions() { + return new Options() + .addOption(Option.builder("h").longOpt("help").desc("Print this message").build()) + .addOption(Option.builder("p").longOpt("path").required().hasArg().desc("The path to the base of the generated java code directory").build()); + } + + + /** + * Executable entry point. + * @param args the arguments for the executable + * @throws IOException on IO error. + */ + protected static void processArgs(final String syntax, + final Function<String, AbstractCodeGenerator<?>> instance, + final String[] args) + throws IOException { + CommandLine commandLine = null; + try { + commandLine = DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter()) + .build().parse(getOptions(), args); + } catch (ParseException pe) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(syntax, pe.getMessage(), getOptions(), ""); + System.exit(1); + } + + if (commandLine.hasOption("h")) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(syntax, getOptions()); + System.exit(0); + } + AbstractCodeGenerator<?> codeGenerator = instance.apply(commandLine.getOptionValue("p")); + codeGenerator.execute(); + } + + /** + * Executes the code generation. + * @throws IOException on IO error + */ + protected abstract void execute() throws IOException; + + /** + * Creates the description for a method. + * @param abstractOption the option generating the method. + * @return the description for the method in {@code AbstractMaven.java}. + */ + protected final String createDesc(final T abstractOption) { + String desc = abstractOption.getDescription(); + if (desc == null) { + throw new IllegalStateException(format("Description for %s may not be null", abstractOption.getName())); + } + if (!desc.contains(".")) { + throw new IllegalStateException(format("First sentence of description for %s must end with a '.'", abstractOption.getName())); + } + if (abstractOption.getArgType() != NONE) { + desc = format("%s Argument%s should be %s%s. (See Argument Types for clarification)", desc, abstractOption.hasArgs() ? "s" : "", + abstractOption.hasArgs() ? "" : "a ", abstractOption.getArgName()); + } + return desc; + } + + /** + * Gets the argument description for the method returned from ${link createMethodName}. + * @param abstractOption the maven option generating the method. + * @param desc the description of the argument. + * @return the argument description for the method in {@code AbstractMaven.java}. + */ + protected String createArgDesc(final T abstractOption, final String desc) { + if (abstractOption.hasArg()) { + String argDesc = desc.substring(desc.indexOf(" ") + 1, desc.indexOf(".") + 1); + return WordUtils.capitalize(argDesc.substring(0, 1)) + argDesc.substring(1); + } else { + return "The state"; + } + } + + /** + * Gets method name for the option. + * @param abstractOption the maven option generating the method. + * @return the method name description for the method in {@code AbstractMaven.java}. + */ + protected abstract String createMethodName(T abstractOption); + + /** + * Gathers all method definitions into a single string. + * @return the definition of all the methods. + */ + protected abstract String gatherMethods(); +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/ArgumentTracker.java b/apache-rat-core/src/main/java/org/apache/rat/ui/ArgumentTracker.java new file mode 100644 index 00000000..d1203b2b --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/ArgumentTracker.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import org.apache.commons.cli.Option; +import org.apache.commons.lang3.StringUtils; +import org.apache.rat.DeprecationReporter; +import org.apache.rat.commandline.Arg; +import org.apache.rat.utils.DefaultLog; +import org.apache.rat.utils.Log; + +/** + * Tracks Arg values that are set and their values for conversion from native UI to + * Apache Commons command line values. + */ +public final class ArgumentTracker { + + /** + * List of deprecated arguments and their deprecation notice. + */ + private final Map<String, String> deprecatedArgs = new HashMap<>(); + + /** + * A map of CLI-based arguments to values. + */ + private final Map<String, List<String>> args = new HashMap<>(); + + /** + * The arguments understood by the UI for the current report execution. + * @param optionCollection The AbstractOptionCollection for this UI. + */ + public ArgumentTracker(final UIOptionCollection<?> optionCollection) { + for (UIOption<?> abstractOption : optionCollection.getMappedOptions().toList()) { + if (abstractOption.isDeprecated()) { + deprecatedArgs.put(abstractOption.getName(), + String.format("Use of deprecated option '%s'. %s", abstractOption.getName(), abstractOption.getDeprecated())); + } + } + } + + /** + * Extract the core name from the option. This is the {@link Option#getLongOpt()} if defined, otherwise + * the {@link Option#getOpt()}. + * @param option the commons cli option. + * @return the common cli based name. + */ + public static String extractKey(final Option option) { + return StringUtils.defaultIfBlank(option.getLongOpt(), option.getOpt()); + } + + /** + * Sets the deprecation report method in the Apache Commons CLI processes. + */ + private void setDeprecationReporter() { + DeprecationReporter.setLogReporter(opt -> { + String msg = deprecatedArgs.get(extractKey(opt)); + if (msg == null) { + DeprecationReporter.getDefault().accept(opt); + } else { + DefaultLog.getInstance().warn(msg); + } + }); + } + + /** + * Gets the list of arguments prepared for the CLI code to parse. + * @return the List of arguments for the CLI command line. + */ + public List<String> args() { + final List<String> result = new ArrayList<>(); + for (Map.Entry<String, List<String>> entry : args.entrySet()) { + result.add("--" + entry.getKey()); + result.addAll(entry.getValue().stream().filter(Objects::nonNull).collect(Collectors.toList())); + } + return result; + } + + /** + * Applies the consumer to each arg and list in turn. + */ + public void apply(final BiConsumer<String, List<String>> consumer) { + args.forEach((key, value) -> consumer.accept(key, new ArrayList<>(value))); + } + + /** + * Validate that the option is defined in Args and has not already been set. + * This check will verify tha only one of the keys in the group can be set. + * @param key the key to check + * @return true if the key may be set. + */ + private boolean validateSet(final String key) { + final Arg arg = Arg.findArg(key); + if (arg != null) { + final Option opt = arg.find(key); + final Option main = arg.option(); + if (opt.isDeprecated()) { + args.remove(extractKey(main)); + // deprecated options must be explicitly set so let it go. + return true; + } + // non-deprecated options may have default so ignore it if another option has already been set. + for (Option o : arg.group().getOptions()) { + if (!o.equals(main)) { + if (args.containsKey(extractKey(o))) { + return false; + } + } + } + return true; + } + return false; + } + + /** + * Set a key and value into the argument list. + * Replaces any existing value. + * @param key the key for the map. + * @param value the value to set. + */ + public void setArg(final UIOption<?> key, final String value) { + setArg(key.keyValue(), value); + } + + /** + * Set a key and value into the argument list. + * Replaces any existing value. + * @param trackerKey the key for the map. + * @param value the value to set. + */ + public void setArg(final String trackerKey, final String value) { + if (value == null || StringUtils.isNotBlank(value)) { + if (validateSet(trackerKey)) { + Option ratOption = Arg.findArg(trackerKey).find(trackerKey); + if (ratOption.hasArg()) { + List<String> values = new ArrayList<>(); + if (DefaultLog.getInstance().isEnabled(Log.Level.DEBUG)) { + DefaultLog.getInstance().debug(String.format("Setting %s to '%s'", trackerKey, value)); + } + values.add(value); + args.put(trackerKey, values); + } else { + DefaultLog.getInstance().warn(String.format("Key '%s' does not accept arguments.", trackerKey)); + } + } else { + DefaultLog.getInstance().warn(String.format("Key '%s' is unknown", trackerKey)); + } + } + } + + /** + * Get the list of values for a key. + * @param key the key for the map. + * @return the list of values for the key or {@code null} if not set. + */ + public Optional<List<String>> getArg(final String key) { + return Optional.ofNullable(args.get(key)); + } + + /** + * Add values to the key in the argument list. + * empty values are ignored. If no non-empty values are present no change is made. + * If the key does not exist, adds it. + * @param option the option to add values for. + * @param value the array of values to set. + */ + public void addArg(final UIOption<?> option, final String... value) { + addArg(option.keyValue(), value); + } + + /** + * Add values to the key in the argument list. + * empty values are ignored. If no non-empty values are present no change is made. + * If the key does not exist, adds it. + * @param trackerKey the key add values for. + * @param value the array of values to set. + */ + public void addArg(final String trackerKey, final String... value) { + List<String> newValues = Arrays.stream(value).filter(StringUtils::isNotBlank).collect(Collectors.toList()); + if (!newValues.isEmpty()) { + if (validateSet(trackerKey)) { + Option ratOption = Arg.findArg(trackerKey).find(trackerKey); + if (ratOption.hasArgs()) { + if (DefaultLog.getInstance().isEnabled(Log.Level.DEBUG)) { + DefaultLog.getInstance().debug(String.format("Adding [%s] to %s", String.join(", ", Arrays.asList(value)), trackerKey)); + } + List<String> values = args.computeIfAbsent(trackerKey, k -> new ArrayList<>()); + values.addAll(newValues); + } else { + DefaultLog.getInstance().warn(String.format("Key '%s' does not accept %sarguments.", trackerKey, + ratOption.hasArg() ? "more that one " : "")); + } + } else { + DefaultLog.getInstance().warn(String.format("Key '%s' is unknown", trackerKey)); + } + } + } + + /** + * Remove a key from the argument list. + * @param option the option to remove the key for. + */ + public void removeArg(final UIOption<?> option) { + args.remove(option.keyValue()); + } + + /** + * Remove a key from the argument list. + * @param trackerKey the key remove. + */ + public void removeArg(final String trackerKey) { + args.remove(trackerKey); + } +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UI.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UI.java new file mode 100644 index 00000000..db1df6a5 --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UI.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui; + +public interface UI<T extends UIOption<T>> { + /** + * Gets the common name of this UI. + * @return the common name of this UI. + */ + default String name() { + return getClass().getSimpleName(); + } + + /** + * Gets the OptionFactory configuration for this UI. + * + * @return the OptionFactory configuration for this UI. + */ + UIOptionCollection<T> getOptionCollection(); +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UIOption.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOption.java new file mode 100644 index 00000000..677eafe2 --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOption.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.cli.Option; +import org.apache.commons.lang3.StringUtils; +import org.apache.rat.OptionCollectionParser; + +import static java.lang.String.format; + +/** + * Abstract class that provides the framework for UI-specific RAT options. + * In this context UI option means an option expressed in the specific UI. + * @param <T> the concrete implementation of AbstractOption. + */ +public abstract class UIOption<T extends UIOption<T>> { + /** The pattern to match CLI options in text */ + protected static final Pattern PATTERN = Pattern.compile("-(-[a-z0-9]+)+"); + /** The actual UI-specific name for the option */ + protected final Option option; + /** The name for the option */ + protected final String name; + /** The argument type for this option */ + protected final OptionCollectionParser.ArgumentType argumentType; + /** The AbstractOptionCollection associated with this AbstractOption */ + protected final UIOptionCollection<T> optionCollection; + + /** + * Constructor. + * + * @param option The CLI option + * @param name the UI-specific name for the option. + */ + protected <C extends UIOptionCollection<T>> UIOption(final C optionCollection, final Option option, final String name) { + this.optionCollection = optionCollection; + this.option = option; + this.name = name; + argumentType = option.hasArg() ? + option.getArgName() == null ? OptionCollectionParser.ArgumentType.ARG : + OptionCollectionParser.ArgumentType.valueOf(option.getArgName().toUpperCase(Locale.ROOT)) : + OptionCollectionParser.ArgumentType.NONE; + } + + /** + * Gets the AbstractOptionCollection that this option is a member of. + * @return the AbstractOptionCollection that this option is a member of. + */ + public final UIOptionCollection<T> getOptionCollection() { + return optionCollection; + } + + /** + * Gets the option this abstract option is wrapping. + * @return the original Option. + */ + public final Option getOption() { + return option; + } + + /** + * Return default value. + * @return default value or {@code null} if no argument given. + */ + public final String getDefaultValue() { + return optionCollection.defaultValue(option); + } + + /** + * Provide means to wrap the given option depending on the UI-specific option implementation. + * @param option The CLI option + * @return the cleaned up option name. + */ + protected abstract String cleanupName(Option option); + + /** + * Gets an example of how to use this option in the native UI. + * @return An example of how to use this option in the native UI. + */ + public abstract String getExample(); + + /** + * Replaces CLI pattern options with implementation specific pattern options. + * @param str the string to clean. + * @return the string with CLI names replaced with implementation specific names. + */ + public String cleanup(final String str) { + String workingStr = str; + if (StringUtils.isNotBlank(workingStr)) { + Map<String, String> maps = new HashMap<>(); + Matcher matcher = PATTERN.matcher(workingStr); + while (matcher.find()) { + String key = matcher.group(); + String optKey = key.substring(2); + Optional<Option> maybeResult = getOptionCollection().getOptions().getOptions().stream() + .filter(o -> optKey.equals(o.getOpt()) || optKey.equals(o.getLongOpt())).findFirst(); + maybeResult.ifPresent(value -> maps.put(key, cleanupName(value))); + } + for (Map.Entry<String, String> entry : maps.entrySet()) { + workingStr = workingStr.replaceAll(Pattern.quote(format("%s", entry.getKey())), entry.getValue()); + } + } + return workingStr; + } + + /** + * Gets the implementation specific name for the CLI option. + * @return The implementation specific name for the CLI option. + */ + public final String getName() { + return name; + } + + /** + * return a string showing long and short options if they are available. Will return + * a string. + * @return A string showing long and short options if they are available. Never {@code null}. + */ + public abstract String getText(); + + /** + * Gets the description in implementation specific format. + * + * @return the description or an empty string. + */ + public final String getDescription() { + return cleanup(option.getDescription()); + } + + /** + * Gets the simple class name for the data type for this option. + * Normally "String". + * @return the simple class name for the type. + */ + public final Class<?> getType() { + return option.hasArg() ? ((Class<?>) option.getType()) : boolean.class; + } + + /** + * Gets the argument name if there is one. + * @return the Argument name + */ + public final String getArgName() { + return argumentType.getDisplayName(); + } + + /** + * Gets the argument type if there is one. + * @return the Argument name + */ + public final OptionCollectionParser.ArgumentType getArgType() { + return argumentType; + } + + /** + * Determines if the option is deprecated. + * @return {@code true} if the option is deprecated + */ + public final boolean isDeprecated() { + return option.isDeprecated(); + } + + /** + * Determines if the option is required. + * @return {@code true} if the option is required. + */ + public final boolean isRequired() { + return option.isRequired(); + } + + /** + * Determine if the enclosed option expects an argument. + * @return {@code true} if the enclosed option expects at least one argument. + */ + public final boolean hasArg() { + return option.hasArg(); + } + + /** + * Returns {@code true} if the option has multiple arguments. + * @return {@code true} if the option has multiple arguments. + */ + public final boolean hasArgs() { + return option.hasArgs(); + } + + /** + * The key value for the option. + * @return the key value for the CLI argument map. + */ + public final String keyValue() { + return StringUtils.defaultIfEmpty(option.getLongOpt(), option.getOpt()); + } + + /** + * Gets the deprecated string if the option is deprecated, or an empty string otherwise. + * @return the deprecated string if the option is deprecated, or an empty string otherwise. + */ + public final String getDeprecated() { + return option.isDeprecated() ? cleanup(StringUtils.defaultIfEmpty(option.getDeprecated().toString(), StringUtils.EMPTY)) : StringUtils.EMPTY; + } +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UIOptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOptionCollection.java new file mode 100644 index 00000000..ab6dcf94 --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOptionCollection.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.rat.Defaults; +import org.apache.rat.commandline.Arg; +import org.apache.rat.utils.Log; + +/** + * A collection of options supported by the UI. This includes RAT options and UI specific options. + * @param <T> the AbstractOption implementation. + */ +public class UIOptionCollection<T extends UIOption<T>> { + /** map of ARG to the associated UpdatableOptionGroup */ + private final Map<Arg, UpdatableOptionGroup> argMap; + /** set of RAT OptionGroups with unsupported options for this UI removed */ + private final UpdatableOptionGroupCollection supportedRatOptions; + /** set of UI specific options */ + private final Map<Option, T> uiOptions; + /** + * Map of option to overridden default value. Generally applies to supported rat options but may be ui + * specific options as well + */ + private final Map <Option, String> defaultValues; + + /** + * The function to generate a concrete BaseOption instance. + */ + private final BiFunction<UIOptionCollection<T>, Option, T> mapper; + + /** + * Construct the UIOptionCollection from the builder. + * @param builder the builder to build from. + */ + protected UIOptionCollection(final Builder<T, ?> builder) { + Objects.requireNonNull(builder.mapper, "Builder.mapper"); + argMap = new TreeMap<>(); + mapper = builder.mapper; + supportedRatOptions = new UpdatableOptionGroupCollection(); + + for (Arg arg : Arg.values()) { + argMap.put(arg, supportedRatOptions.add(arg.group())); + } + + for (Option opt : builder.unsupportedRatOptions) { + supportedRatOptions.findGroups(opt).forEach(group -> group.disableOption(opt)); + } + uiOptions = new HashMap<>(); + supportedRatOptions.options().getOptions() + .forEach(option -> uiOptions.put(option, mapper.apply(this, option))); + builder.uiOptions.stream().filter(option -> !uiOptions.containsKey(option)) + .forEach(option -> uiOptions.put(option, mapper.apply(this, option))); + defaultValues = new HashMap<>(builder.defaultValues); + } + + /** + * Checks if an Arg is selected. + * @param arg the Arg to check. + * @return {@code true} if the arg is selected. + */ + public final boolean isSelected(final Arg arg) { + UpdatableOptionGroup group = argMap.get(arg); + return group != null && group.getSelected() != null; + } + + /** + * Gets the selected Option for the arg. + * @param arg the arg to check. + * @return an Optional containing the selected option, or an empty Optional if none was selected. + */ + public final Optional<Option> getSelected(final Arg arg) { + UpdatableOptionGroup group = argMap.get(arg); + String s = group == null ? null : group.getSelected(); + if (s != null) { + for (Option result : group.getOptions()) { + if (result.getKey().equals(s)) { + return Optional.of(result); + } + } + } + return Optional.empty(); + } + + /** + * Gets the collection of unsupported Options. + * @return the Options comprised for the unsupported options. + */ + public final Options getUnsupportedOptions() { + return supportedRatOptions.unsupportedOptions(); + } + + /** + * Gets the UiOption instance for the Option. + * @param option the option to find the instance of. + * @return an UIOption instance that wraps the option. + */ + public final T getMappedOption(final Option option) { + return uiOptions.get(option); + } + + /** + * Gets an Options that contains the RAT Arg defined Option instances that are understood by this collection. + * OptionGroups are registered in the resulting Options object. + * @return an Options that contains the RAT Arg defined Option instances that are understood by this collection. + */ + public final Options getOptions() { + return supportedRatOptions.options().addOptions(additionalOptions()); + } + + /** + * Gets the Stream of AbstractOption implementations understood by this collection. + * @return the Stream of AbstractOption implementations understood by this collection. + */ + public final Stream<T> getMappedOptions() { + return uiOptions.values().stream(); + } + + /** + * Gets a map client option name to specified AbstractOption implementation. + * @return a map client option name to specified AbstractOption implementation + */ + public final Map<String, T> getOptionMap() { + Map<String, T> result = new TreeMap<>(); + getMappedOptions().forEach(mappedOption -> result.put(ArgumentTracker.extractKey(mappedOption.getOption()), mappedOption)); + return result; + } + + /** + * Gets the additional options understood by this collection. + * @return the additional options understood by this collection. + */ + public final Options additionalOptions() { + Options options = new Options(); + uiOptions.keySet().stream() + .filter(option -> !supportedRatOptions.contains(option)) + .forEach(options::addOption); + return options; + } + + /** + * Gets the default value for the option. + * @param option the option to lookup. + * @return the default value or {@code null} if not set. + */ + public final String defaultValue(final Option option) { + return defaultValues.get(option); + } + + /** + * Builder for a BaseOptionCollection. + * @param <T> the concreate type of the BaseOption. + * @param <S> the concrete type being built. + */ + protected static class Builder<T extends UIOption<T>, S extends Builder<T, S>> { + /** set of additional UI specific options */ + private final List<Option> uiOptions; + /** + * Map of option to overridden default value. Generally applies to supported rat options but may be ui + * specific options as well + */ + private final Map <Option, String> defaultValues; + /** The list of unsupported Rat options */ + protected final List<Option> unsupportedRatOptions; + /** The function to convert an option into a UIOption. */ + private BiFunction<UIOptionCollection<T>, Option, T> mapper; + + /** + * Constructor for the builder. + */ + protected Builder() { + uiOptions = new ArrayList<>(); + defaultValues = new HashMap<>(); + unsupportedRatOptions = new ArrayList<>(); + defaultValue(Arg.LOG_LEVEL, Log.Level.WARN.name()); + defaultValue(Arg.OUTPUT_ARCHIVE, Defaults.ARCHIVE_PROCESSING.name()); + defaultValue(Arg.OUTPUT_STANDARD, Defaults.STANDARD_PROCESSING.name()); + defaultValue(Arg.OUTPUT_LICENSES, Defaults.LIST_LICENSES.name()); + defaultValue(Arg.OUTPUT_FAMILIES, Defaults.LIST_FAMILIES.name()); + } + + /** + * build the UIOptionCollection. + * @return the UIOptionCollection. + */ + public UIOptionCollection<T> build() { + return new UIOptionCollection<>(this); + } + + /** + * Returns this cast to {@code <S>} class. + * @return this as {@code <S>} class. + */ + protected final S self() { + return (S) this; + } + + /** + * Set the mapper for the builder. + * @param mapper the function to convert an option into a UIOption ({@code <T>} object). + * @return this + */ + public S mapper(final BiFunction<UIOptionCollection<T>, Option, T> mapper) { + this.mapper = mapper; + return self(); + } + + /** + * Add a UI option to the collection. + * @param uiOption the UI Option to add. + * @return this + */ + public S uiOption(final Option uiOption) { + uiOptions.add(uiOption); + return self(); + } + + /** + * Add a UI options to the collection. + * @param uiOption the UIOptions ({@code <T>} objects) to add. + * @return this + */ + public S uiOptions(final Option... uiOption) { + uiOptions.addAll(Arrays.asList(uiOption)); + return self(); + } + + /** + * Register an option as unsupported. + * @param option the option that is not be supported. This should be an option in the + * {@link Arg} collection. + * @return this + */ + public S unsupported(final Option option) { + unsupportedRatOptions.add(option); + return self(); + } + + /** + * Register multiple options as unsupported. + * Will ignore all the options associated with the specified Arg. + * @param arg The Arg to ignore. + * @return this + */ + public S unsupported(final Arg arg) { + unsupportedRatOptions.addAll(arg.group().getOptions()); + return self(); + } + + /** + * Specify the default values for an option. + * @param option the option to specify the default value for. + * @param value the value for the option. + * @return this + */ + public S defaultValue(final Option option, final String value) { + defaultValues.put(option, value); + return self(); + } + + /** + * Specify the default values for an Arg. + * @param arg the Arg to specify the default value for. + * @param value the value for the option. + * @return this + */ + public S defaultValue(final Arg arg, final String value) { + return defaultValue(arg.option(), value); + } + } +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroup.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroup.java new file mode 100644 index 00000000..e936357e --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroup.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; + +/** + * An implementation of Apache Commons CLI OptionGroup that allows options to be removed (disabled). + */ +public final class UpdatableOptionGroup extends OptionGroup { + /** The set of options to remove */ + private final Set<Option> disabledOptions = new HashSet<>(); + + /** + * Converts the group into an UpdatableOptionGroup if it is not already an instance + * @param group the group to convert. + * @return an UpdatableOptionGroup. + */ + public static UpdatableOptionGroup create(final OptionGroup group) { + return group instanceof UpdatableOptionGroup ? (UpdatableOptionGroup) group : new UpdatableOptionGroup(group); + } + + private UpdatableOptionGroup(final OptionGroup group) { + group.getOptions().forEach(super::addOption); + } + + /** + * Disable an option in the group. + * @param option The option to disable. + */ + public void disableOption(final Option option) { + disabledOptions.add(option); + } + + public boolean isEmpty() { + return getOptions().isEmpty(); + } + + /** + * Gets the disabled options for this group. + * @return the set of disabled options for this group. + */ + public Stream<Option> getDisableOptions() { + return disabledOptions.stream(); + } + /** + * Reset the group so that all disabled options are re-enabled. + */ + public void reset() { + disabledOptions.clear(); + } + + @Override + public Collection<Option> getOptions() { + return super.getOptions().stream().filter(opt -> !disabledOptions.contains(opt)).toList(); + } + + @Override + public UpdatableOptionGroup addOption(final Option option) { + super.addOption(option); + return this; + } +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroupCollection.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroupCollection.java new file mode 100644 index 00000000..893db66a --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroupCollection.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ +package org.apache.rat.ui; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; + +/** + * A collection of UpdatableOptionGroups. + */ +public class UpdatableOptionGroupCollection { + /** the contained UpdatableOptionGroups */ + private final List<UpdatableOptionGroup> updatableOptionGroups; + + /** + * Creates an empty collection. + */ + public UpdatableOptionGroupCollection() { + updatableOptionGroups = new ArrayList<>(); + } + + /** + * Adds an OptionGroup to the collection. If the OptionGroup is not an UpdatableOptionGroup + * it is converted first. + * @param optionGroup an OptionGroup to add. + * @return the UpdatableOptionGroup that was added. + */ + public UpdatableOptionGroup add(final OptionGroup optionGroup) { + UpdatableOptionGroup uog = UpdatableOptionGroup.create(optionGroup); + updatableOptionGroups.add(uog); + return uog; + } + + /** + * Gets an Options object from this collection. + * @return an Options object. + */ + public Options options() { + Options result = new Options(); + updatableOptionGroups.forEach(result::addOptionGroup); + return result; + } + + /** + * Gets ll the UpdatableOptionGroups that the option is in. + * @param option the option to searhc for. + * @return the stream of UpdatableOptionGroups the option is in. + */ + public Stream<UpdatableOptionGroup> findGroups(final Option option) { + return updatableOptionGroups.stream().filter(og -> og.getOptions().contains(option)); + } + + /** + * Gets the set of removed Options from the collection. + * @return the set of removed options. + */ + public Set<Option> removedOptions() { + Set<Option> result = new HashSet<>(); + updatableOptionGroups.forEach(uog -> uog.getDisableOptions().forEach(result::add)); + return result; + } + + /** + * Gets the unsupported options + * If multiple options from the a group are disabled they will be added to the + * options in a group together. + * @return the Options object containing all the unsupported options. + */ + public Options unsupportedOptions() { + Options result = new Options(); + for (UpdatableOptionGroup uog : updatableOptionGroups) { + OptionGroup group = new OptionGroup(); + uog.getDisableOptions().forEach(group::addOption); + result.addOptionGroup(group); + } + return result; + } + + /** + * Returns true if the option is in any of the groups. + * @param option the option. + * @return {@code true} if the option is in any of the groups. + */ + public boolean contains(final Option option) { + return updatableOptionGroups.stream().anyMatch(og -> og.getOptions().contains(option)); + } +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/package-info.java b/apache-rat-core/src/main/java/org/apache/rat/ui/package-info.java new file mode 100644 index 00000000..765d92c1 --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + */ + +/** + * Classes that support UI generation and interoperability. + */ +package org.apache.rat.ui; diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/spi/UIProvider.java b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/UIProvider.java new file mode 100644 index 00000000..9870b3cf --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/UIProvider.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui.spi; + +import org.apache.rat.ui.UI; + +public interface UIProvider { + UI<?> create(); +} diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/spi/package-info.java b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/package-info.java new file mode 100644 index 00000000..9d724675 --- /dev/null +++ b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +/** + * The SPI implementation for the UIs + */ +package org.apache.rat.ui.spi; diff --git a/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java b/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java index 7f27cb8d..28e856b0 100644 --- a/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java +++ b/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java @@ -23,6 +23,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.rat.CLIOptionCollection; import org.apache.rat.DeprecationReporter; import org.apache.rat.OptionCollection; import org.apache.rat.ReportConfiguration; @@ -58,7 +59,7 @@ public class ArgTests { CommandLine commandLine = createCommandLine(new String[] {"--output-file", fileName}); OutputFileConfig configuration = new OutputFileConfig(); ArgumentContext ctxt = new ArgumentContext(new File("."), configuration, commandLine); - Arg.processArgs(ctxt); + Arg.processArgs(ctxt, CLIOptionCollection.INSTANCE); assertThat(configuration.actual.getAbsolutePath()).isEqualTo(expected.getCanonicalPath()); } } diff --git a/apache-rat-core/src/test/java/org/apache/rat/ui/ArgumentTrackerTest.java b/apache-rat-core/src/test/java/org/apache/rat/ui/ArgumentTrackerTest.java new file mode 100644 index 00000000..80c5e1cc --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/ui/ArgumentTrackerTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import org.apache.commons.cli.Option; +import org.apache.rat.commandline.Arg; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.apache.rat.ui.UIOptionCollectionTest.TestingUIOptionCollection; +import static org.apache.rat.ui.UIOptionCollectionTest.TestingUIOption; + +public class ArgumentTrackerTest { + private ArgumentTracker underTest; + private TestingUIOptionCollection testingUIOptionCollection; + + @BeforeEach + public void setUp() { + testingUIOptionCollection = new TestingUIOptionCollection(); + underTest = new ArgumentTracker(testingUIOptionCollection); + } + + @Test + void extractKey() { + assertThat(ArgumentTracker.extractKey(Option.builder().longOpt("foo").build())).isEqualTo("foo"); + assertThat(ArgumentTracker.extractKey(Option.builder("b").build())).isEqualTo("b"); + assertThat(ArgumentTracker.extractKey(Option.builder("b").longOpt("foo").build())).isEqualTo("foo"); + assertThatThrownBy(() -> ArgumentTracker.extractKey(Option.builder().build())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Either opt or longOpt must be specified"); + } + + @Test + void args() { + // no args to start + assertThat(underTest.args()).isEmpty(); + Option option = findOptionWithArgs(1); + String string1 = String.format("--%s foo", ArgumentTracker.extractKey(option)); + TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option); + underTest.setArg(mappedOption, "foo"); + option = findOptionWithArgs(2); + String string2 = String.format("--%s bar baz", ArgumentTracker.extractKey(option)); + mappedOption = testingUIOptionCollection.getMappedOption(option); + underTest.addArg(mappedOption, "bar"); + underTest.addArg(mappedOption, "baz"); + String join = String.join(" ", underTest.args()); + assertThat(join).contains(string1); + assertThat(join).contains(string2); + } + + @Test + void setArg() { + Option option = findOptionWithArgs(1); + TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option); + underTest.setArg(mappedOption, "foo"); + assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo")); + } + + private Option findOptionWithArgs(int number) { + Predicate<Option> filter; + if (number <= 0) { + filter = opt -> !opt.hasArg(); + } else if (number == 1) { + filter = opt -> opt.hasArg() && ! opt.hasArgs(); + } else { + filter = opt -> opt.hasArgs(); + } + return Arrays.stream(Arg.values()).map(Arg::option).filter(filter).findAny().orElseThrow(); + } + + @Test + void addArg() { + Option option = findOptionWithArgs(2); + TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option); + underTest.addArg(mappedOption, "foo"); + assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo")); + underTest.addArg(mappedOption, "bar"); + assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo", "bar")); + } + + @Test + void setOverridesAddArg() { + Option option = findOptionWithArgs(2); + TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option); + underTest.addArg(mappedOption, "foo"); + assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo")); + underTest.addArg(mappedOption, "bar"); + assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo", "bar")); + underTest.setArg(mappedOption, "baz"); + assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("baz")); + } + + @Test + void invalidAbstractOption() { + Option option = Option.builder().longOpt("notAValidOption").build(); + TestingUIOption invalidOption = new TestingUIOption(testingUIOptionCollection, option); + underTest.addArg(invalidOption, "foo"); + assertThat(underTest.getArg(invalidOption.keyValue())).isEmpty(); + } + +} diff --git a/apache-rat-core/src/test/java/org/apache/rat/ui/UIOptionCollectionTest.java b/apache-rat-core/src/test/java/org/apache/rat/ui/UIOptionCollectionTest.java new file mode 100644 index 00000000..9325df75 --- /dev/null +++ b/apache-rat-core/src/test/java/org/apache/rat/ui/UIOptionCollectionTest.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.rat.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; +import org.apache.commons.cli.AlreadySelectedException; +import org.apache.commons.cli.DeprecatedAttributes; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.OptionGroup; +import org.apache.commons.cli.Options; +import org.apache.rat.commandline.Arg; +import org.apache.rat.utils.CasedString; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UIOptionCollectionTest { + + public static final Option UI_OPTION = Option.builder("ui-option1").build(); + public static final Option DEPRECATED_UI_OPTION = Option.builder("ui-option2").deprecated().build(); + + public static class TestingUIOptionCollection extends UIOptionCollection<TestingUIOption> { + public TestingUIOptionCollection() { + super(new Builder()); + + } + private static class Builder extends UIOptionCollection.Builder<TestingUIOption, Builder> { + Builder() { + mapper(TestingUIOption::new) + .uiOption(UI_OPTION) + .uiOption(DEPRECATED_UI_OPTION) + .unsupported(Arg.COUNTER_MAX) + .unsupported(Arg.EXCLUDE.option()) + .defaultValue(UI_OPTION, "foo"); + + } + } + } + + static class TestingUIOption extends UIOption<TestingUIOption> { + + TestingUIOption(final UIOptionCollection<TestingUIOption> collection, final Option option) { + super(collection, option, new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(option)).toCase(CasedString.StringCase.DOT)); + } + + @Override + protected String cleanupName(Option option) { + return new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(option)).toCase(CasedString.StringCase.DOT); + } + + @Override + public String getExample() { + return String.format("The example for $s", cleanupName(option)); + } + + @Override + public String getText() { + return String.format("Short and long options for ", cleanupName(option)); + } + } + + private final TestingUIOptionCollection underTest = new TestingUIOptionCollection(); + + private Optional<Option> findDeprecatedArgOption() { + Collection<Option> options = underTest.getOptions().getOptions(); + return Arg.getOptions().getOptions().stream().filter(Option::isDeprecated) + .filter(option -> options.contains(option)).findAny(); + } + + private Optional<Option> findDeprecatedOption() { + Collection<Option> options = underTest.getOptions().getOptions(); + return Arg.getOptions().getOptions().stream().filter(Option::isDeprecated) + .filter(option -> !options.contains(option)).findAny(); + } + + @Test + void getMappedOption() { + TestingUIOption one = underTest.getMappedOption(UI_OPTION); + assertThat(one.option).isEqualTo(UI_OPTION); + assertThat(one.name).isEqualTo("ui.option1"); + assertThat(one.isDeprecated()).isFalse(); + TestingUIOption two = underTest.getMappedOption(DEPRECATED_UI_OPTION); + assertThat(two.option).isEqualTo(DEPRECATED_UI_OPTION); + assertThat(two.name).isEqualTo("ui.option2"); + assertThat(two.isDeprecated()).isTrue(); + + assertThat(underTest.getMappedOption(Arg.EXCLUDE.option())).isNull(); + for (Option option : Arg.COUNTER_MAX.group().getOptions()) { + assertThat(underTest.getMappedOption(option)).isNull(); + } + + TestingUIOption config = underTest.getMappedOption(Arg.CONFIGURATION.option()); + assertThat(config).isNotNull(); + assertThat(config.option).isEqualTo(Arg.CONFIGURATION.option()); + assertThat(config.name).isEqualTo("config"); + + config = underTest.getMappedOption(Option.builder("foo").build()); + assertThat(config).isNull(); + } + + @Test + void getSelected() throws AlreadySelectedException { + assertThat(underTest.isSelected(Arg.CONFIGURATION)).isFalse(); + assertThat(underTest.getSelected(Arg.CONFIGURATION)).isEmpty(); + + Option option = Arg.CONFIGURATION.option(); + OptionGroup group = underTest.getOptions().getOptionGroup(Arg.CONFIGURATION.option()); + group.setSelected(option); + + assertThat(underTest.isSelected(Arg.CONFIGURATION)).isTrue(); + assertThat(underTest.getSelected(Arg.CONFIGURATION)).contains(option); + } + + @Test + void getUnsupportedOptions() { + Options options = underTest.getUnsupportedOptions(); + Collection<Option> OptionCollection = options.getOptions(); + List<Option> expected = new ArrayList<Option>(); + expected.addAll(Arg.COUNTER_MAX.group().getOptions()); + expected.add(Arg.EXCLUDE.option()); + assertThat(options.getOptions()).containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + void getAdditionalOptions() { + assertThat(underTest.additionalOptions().getOptions()).containsExactlyInAnyOrder(UI_OPTION, DEPRECATED_UI_OPTION); + } + + @Test + void defaultValue() { + assertThat(underTest.defaultValue(UI_OPTION)).isEqualTo("foo"); + assertThat(underTest.defaultValue(DEPRECATED_UI_OPTION)).isNull(); + } +} diff --git a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java b/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java index fc00e5e1..5ac78b82 100644 --- a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java +++ b/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java @@ -7,7 +7,7 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an diff --git a/pom.xml b/pom.xml index 253189e0..8865a5f0 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,16 @@ agnostic home for software distribution comprehension and audit tools. </distributionManagement> <dependencyManagement> <dependencies> + <dependency> + <groupId>com.github.spotbugs</groupId> + <artifactId>spotbugs-annotations</artifactId> + <version>4.9.8</version> + </dependency> + <dependency> + <groupId>org.reflections</groupId> + <artifactId>reflections</artifactId> + <version>0.10.2</version> + </dependency> <!-- used to render the site and make skin updates more transparent --> <dependency> <groupId>org.apache.maven.skins</groupId>
