UIOption.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.ui;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
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.OptionCollection;
import org.apache.rat.utils.CasedString;

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-Za-z0-9]+-?)+"); // NOSONAR
    /**
     * The actual UI-specific name for the option
     */
    protected final Option option;
    /**
     * The name for the option
     */
    protected final CasedString name;
    /**
     * The argument type for this option
     */
    protected final OptionCollection.ArgumentType argumentType;
    /**
     * The AbstractOptionCollection associated with this AbstractOption
     */
    protected final UIOptionCollection<T> optionCollection;

    /**
     * Constructor.
     *
     * @param builder UIOption builder.
     */
    protected UIOption(final Builder<T, ?> builder) {
        this.optionCollection = builder.optionCollection;
        this.option = builder.option;
        this.name = builder.name;

        OptionCollection.ArgumentType argType;
        if (option.hasArg()) {
            if (option.getArgName() == null) {
                argType = OptionCollection.ArgumentType.ARG;
            } else {
                // extract the name of the argument type.
                try {
                    argType = OptionCollection.ArgumentType.valueOf(option.getArgName().toUpperCase(Locale.ROOT));
                } catch (IllegalArgumentException e) {
                    argType = OptionCollection.ArgumentType.ARG;
                }
            }
        } else {
            argType = OptionCollection.ArgumentType.NONE;
        }
        this.argumentType = argType;
    }

    @Override
    public boolean equals(final Object o) {
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        UIOption<?> uiOption = (UIOption<?>) o;
        return getOption().equals(uiOption.getOption()) && name.equals(uiOption.name) && argumentType == uiOption.argumentType;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    /**
     * Gets the AbstractOptionCollection that this option is a member of.
     * @return the AbstractOptionCollection that this option is a member of.
     * @param <X> The collection type to return.
     */
    public final <X extends UIOptionCollection<T>> X getOptionCollection() {
        return (X) 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(1 == key.indexOf('-', 1) ?  2 : 1);
                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 native UI.
     * @return The implementation specific name for the native UI.
     */
    public final String getName() {
        return name.toString();
    }

    /**
     * Gets the CasedString version of the name for the native UI.
     * @return the CasedString version of the name for the native UI.
     */
    public final CasedString getCasedName() {
        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 OptionCollection.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;
    }

    /**
     * The abstract UIOption Builder.
     * @param <T> the concrete type of the UIOption this builder produces.
     * @param <B> the concrete type of this builder.
     */
    public abstract static class Builder<T extends UIOption<T>, B extends Builder<T, B>> {
        /**
         * The collection to add the new UI Option to.
         */
        private UIOptionCollection<T> optionCollection;
        /**
         * THe base option that is being mapped.
         */
        private Option option;
        /**
         * The UI name of this option.
         */
        private CasedString name = CasedString.NULL;
        /**
         * Constructor.
         */
        protected Builder() {
        }

        /**
         * Returns a function to convert an Option to a CasedString that is the native name for the option.
         * @return a function to convert an Option to a CasedString that is the native name for the option.
         */
        protected abstract Function<Option, CasedString> getNameFactory();

        /**
         * Gets the option.
         * @return the option or {@code null} if the option is not set.
         */
        protected final Option option() {
            return option;
        }

        /**
         * Gets the UIOptionCollection that the options will be added to.
         * @return UIOptionCollection that the options will be added to.
         */
        protected final UIOptionCollection<T> optionCollection() {
            return optionCollection;
        }

        /**
         * Returns this builder cast to the builder type.
         * Useful for implementing fluent builders.
         * @return this builder.
         */
        protected final B self() {
            return (B) this;
        }

        /**
         * Sets the UIOptionCollection that the UIOptions will be added to.
         * @param optionCollection the UIOptionCollection that the UIOptions will be added to.
         * @return this
         */
        public B optionCollection(final UIOptionCollection<T> optionCollection) {
            this.optionCollection = optionCollection;
            return self();
        }

        /**
         * Sets the Option that the UIOption will be generated from.
         * @param option to build the UIOption from.
         * @return this
         */
        public B option(final Option option) {
            Objects.requireNonNull(option, "Option may not be null");
            this.option = option;
            this.name = getNameFactory().apply(option);
            if (this.name == null || this.name.isNull()) {
                throw new IllegalArgumentException("name for " + option + " may not be null or contain a null value");
            }
            return self();
        }

        /**
         * Executes the final build.
         * @return An instance of the UIOption.
         * @throws IllegalArgumentException if values are not set correctly.
         */
        protected abstract T doBuild() throws IllegalArgumentException;

        /**
         * Builds the UIOption.
         * @return the UIOption.
         * @throws IllegalArgumentException if values are not set correctly.
         */
        public final T build() throws IllegalArgumentException {
            Objects.requireNonNull(optionCollection, "OptionCollection may not be null");
            Objects.requireNonNull(option, "Option may not be null");
            if (name == null || name.isNull()) {
                throw new IllegalArgumentException("name must not be null or contain a null value");
            }
            return doBuild();
        }
    }
}