ArgumentContext.java

/*
 * 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.commandline;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

import org.apache.commons.cli.AlreadySelectedException;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.apache.rat.ConfigurationException;
import org.apache.rat.OptionCollectionParser;
import org.apache.rat.ReportConfiguration;
import org.apache.rat.document.DocumentName;
import org.apache.rat.ui.UIOptionCollection;
import org.apache.rat.utils.DefaultLog;
import org.apache.rat.utils.Log;

import static java.lang.String.format;

/**
 * Provides the context necessary to process various arguments.
 * @since 0.17
 */
public final class ArgumentContext {
    /** The report configuration that is being built */
    private final ReportConfiguration configuration;
    /** The command line that is building the configuration */
    private final CommandLine commandLine;
    /** The directory from which relative file names will be resolved */
    private final DocumentName workingDirectory;

    /**
     * Creates a context with the specified configuration.
     * @param workingDirectory the directory from which relative file names will be resolved.
     * @param configuration the configuration that is being built.
     * @param opts the Options for the command line.
     * @param args the arguments for the options.
     * @throws ParseException if the options can not parse the arguments.
     */
    public ArgumentContext(final File workingDirectory, final ReportConfiguration configuration, final Options opts, final String[] args)
            throws ParseException {
        this.workingDirectory = DocumentName.builder(workingDirectory).build();
        this.commandLine = OptionCollectionParser.parseCommandLine(clearSelected(opts), args);
        this.configuration = configuration;
    }

    /**
     * Creates a context with an empty configuration.
     * @param workingDirectory the directory from which to resolve relative file names.
     * @param opts the Options for the command line.
     * @param args the arguments for the options.
     * @throws ParseException if the options can not parse the arguments.
     */
    public ArgumentContext(final File workingDirectory, final Options opts, final String[] args) throws ParseException {
        this(workingDirectory, new ReportConfiguration(), opts, args);
    }

    /**
     * Gets a logging/debug string describing the Option.
     * @param option the option to describe.
     * @return the logging/debug string describing the Option.
     */
    public static String toString(final Option option) {
        return String.format("Option[%s v:[%s]",
                StringUtils.defaultIfEmpty(option.getLongOpt(), option.getOpt()),
                option.getValues() == null ? "[]" : Arrays.toString(option.getValues())
                );
    }
    /**
     * Clears the group selections in the options.
     * @param options the options to clear.
     * @return the options with all selections cleared.
     */
    private static Options clearSelected(final Options options) {
        for (Option opt : options.getOptions()) {
            OptionGroup group = options.getOptionGroup(opt);
            if (group != null) {
                try {
                    group.setSelected(null);
                } catch (AlreadySelectedException e) {
                    throw new RuntimeException("Unexpected error with null argument", e);
                }
            }
        }
        return options;
    }

    /**
     * Process the arguments specified in this context.
     */
    public void processArgs(final UIOptionCollection<?> uiOptionCollection) {
        Arg.processArgs(this, uiOptionCollection);
    }

    /**
     * Gets the configuration.
     * @return the configuration that is being built.
     */
    public ReportConfiguration getConfiguration() {
        return configuration;
    }

    /**
     * Gets the Options from the command line.
     * @return the Options from the command line.
     */
    public Option[] getOptions() {
        return commandLine.getOptions();
    }

    /**
     * Gets the arguments that followed all options on the command line.
     * @return the arguments that followed all options.
     */
    public List<String> getArgs() {
        return commandLine.getArgList();
    }

    /**
     * Determines if option was specified on the command line.
     * @param option the option to check.
     * @return {@code true}, if option was specified on the command line.
     */
    public boolean hasOption(final Option option) {
        return commandLine.hasOption(option);
    }

    /**
     * Determines if option was specified on the command line.
     * @param option the option to check.
     * @return {@code true}, if option was specified on the command line.
     */
    public boolean hasOption(final String option) {
        return commandLine.hasOption(option);
    }

    /**
     * Gets the option value or {@code null} if it is not set on the command line.
     * @param selected the option to retrieve the value for.
     * @return the option value or {@code null} if it is not set.
     */
    public String getOptionValue(final Option selected) {
        return commandLine.getOptionValue(selected);
    }

    /**
     * Gets the list of option values from the command line.
     * @param selected the option to get values for.
     * @return The list of options from the command line. Might be an empty list but never {@code null}.
     */
    public List<String> getOptionValues(final Option selected) {
        String[] result = commandLine.getOptionValues(selected);
        return result == null ? Collections.emptyList() : List.of(result);
    }

    /**
     * Gets the parsed option value from the command line.
     * @param selected the option to get value for.
     * @return the parsed value or {@code null} if not found.
     * @param <T> the expected parsed value type.
     */
    public <T> T getParsedOptionValue(final Option selected) {
        return this.getParsedOptionValue(selected, () -> null);
    }

    /**
     * Gets the parsed option value from the command line.
     * @param selected the option to get value for.
     * @param defaultSupplier a supplier of default values.
     * @return the parsed value or default value if not found.
     * @param <T> the expected parsed value type.
     */
    public <T> T getParsedOptionValue(final Option selected, final Supplier<T> defaultSupplier) {
        Objects.requireNonNull(selected);
        Objects.requireNonNull(defaultSupplier);
        try {
            return commandLine.getParsedOptionValue(selected, defaultSupplier);
        } catch (ParseException e) {
            logParseException(e, selected);
            throw new ConfigurationException(format("'%s' converter '%s' does not produce a class of type %s",
                    toString(selected),
                    selected.getConverter().getClass().getName(),
                    selected.getType()), e);
        }
    }

    /**
     * Gets the parsed option values from the command line.
     * @param selected the option to get value for.
     * @return the parsed value list an empty list if not found.
     * @param <T> the expected parsed value type.
     */
    public <T> List<T> getParsedOptionValues(final Option selected) {
        return this.getParsedOptionValues(selected, Collections::emptyList);
    }

    /**
     * Gets the parsed option values from the command line.
     * @param selected the option to get value for.
     * @param defaultSupplier a supplier of default value list.
     * @return the parsed value list or default value if not found.
     * @param <T> the expected parsed value type.
     */
    public <T> List<T> getParsedOptionValues(final Option selected, final Supplier<List<T>> defaultSupplier) {
        Objects.requireNonNull(selected);
        Objects.requireNonNull(defaultSupplier);

        List<String> strings = getOptionValues(selected);
        if (strings.isEmpty()) {
            return defaultSupplier.get();
        }
        List<T> result = new ArrayList<>();
        for (String value : strings) {
            try {
                result.add((T) selected.getConverter().apply(value));
            } catch (Throwable e) { // NOSONAR - need to catch Throwable here due to CommonsCLI's API
                if (e instanceof Error err) {
                    throw err;
                }
                throw new ConfigurationException(format("'%s' converter '%s' does not produce a class of type %s",
                        toString(selected),
                        selected.getConverter().getClass().getName(),
                        selected.getType()), e);
            }
        }
        return result;
    }

    /**
     * Gets the directory name from which relative file names will be resolved.
     * @return the directory name from which relative file names will be resolved.
     */
    public DocumentName getWorkingDirectory() {
        return workingDirectory;
    }

    /**
     * Logs a ParseException as a warning and the exception itself as a debug.
     * @param exception the parse exception to log.
     * @param opt the option being processed.
     */
    public void logParseException(final ParseException exception, final Option opt) {
        DefaultLog.getInstance().warn(format("Invalid %s specified: %s ", toString(opt), commandLine.getOptionValue(opt)));
        if (DefaultLog.getInstance().isEnabled(Log.Level.DEBUG)) {
            DefaultLog.getInstance().debug(exception);
        }
    }

    /**
     * Creates a visual description of the CommandLine.
     * This used to be provided by the {@code CommandLine.toString()}
     * method, but that has been removed.
     * @param commandLine the command line to format.
     * @return a String representation of the command line suitable for debugging.
     */
    public static String commandLineDescription(final CommandLine commandLine) {
        List<String> options = new ArrayList<>();
        for (Option opt : commandLine.getOptions()) {
            options.add(String.format("Option[%s v:[%s]]", StringUtils.defaultIfEmpty(opt.getLongOpt(), opt.getKey()),
                    String.join(",", opt.getValues() == null ? new String [0] : opt.getValues())));
        }
        return new StringBuilder()
                .append("[ CommandLine: [ options: ")
                .append(options)
                .append(" ] [ args: ")
                .append(commandLine.getArgList().toString())
                .append(" ] ]")
                .toString();
    }
}