ArgumentTracker.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
 *
 *   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 org.apache.commons.cli.Option;
import org.apache.commons.lang3.StringUtils;
import org.apache.rat.commandline.Arg;
import org.apache.rat.utils.CasedString;
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());
    }

    /**
     * Generates the CasedString for the specified option.
     * @param option the option to extract the name for.
     * @return the CasedString in KEBAB format.
     */
    public static CasedString extractName(final Option option) {
        return new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(option));
    }

    /**
     * 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).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 that only one of the keys in the group can be set.
     * @param key the key to check
     * @return {@code 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) && 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).toList();
        if (newValues.isEmpty()) {
            return;
        }
        if (!validateSet(trackerKey)) {
            DefaultLog.getInstance().warn(String.format("Key '%s' is unknown", trackerKey));
            return;
        }
        Option ratOption = Arg.findArg(trackerKey).find(trackerKey);
        if (!ratOption.hasArgs()) {
            DefaultLog.getInstance().warn(String.format("Key '%s' does not accept %sarguments.", trackerKey,
                    ratOption.hasArg() ? "more that one " : ""));
        }
        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);
    }

    /**
     * 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);
    }
}