CommonsCliOptions.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.maven.cling.invoker;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.DeprecatedAttributes;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.ParseException;
import org.apache.maven.api.cli.Options;
import org.apache.maven.api.cli.ParserRequest;
import org.apache.maven.api.services.Interpolator;
import org.apache.maven.api.services.InterpolatorException;
import org.apache.maven.jline.MessageUtils;

import static java.util.Objects.requireNonNull;
import static org.apache.maven.cling.invoker.CliUtils.createInterpolator;
import static org.apache.maven.cling.invoker.CliUtils.toMap;

public class CommonsCliOptions implements Options {
    public static CommonsCliOptions parse(String source, String[] args) throws ParseException {
        CLIManager cliManager = new CLIManager();
        return new CommonsCliOptions(source, cliManager, cliManager.parse(args));
    }

    protected final String source;
    protected final CLIManager cliManager;
    protected final CommandLine commandLine;

    protected CommonsCliOptions(String source, CLIManager cliManager, CommandLine commandLine) {
        this.source = requireNonNull(source);
        this.cliManager = requireNonNull(cliManager);
        this.commandLine = requireNonNull(commandLine);
    }

    @Override
    public String source() {
        return source;
    }

    @Override
    public Optional<Map<String, String>> userProperties() {
        if (commandLine.hasOption(CLIManager.USER_PROPERTY)) {
            return Optional.of(toMap(commandLine.getOptionProperties(CLIManager.USER_PROPERTY)));
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> showVersionAndExit() {
        if (commandLine.hasOption(CLIManager.SHOW_VERSION_AND_EXIT)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> showVersion() {
        if (commandLine.hasOption(CLIManager.SHOW_VERSION)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> quiet() {
        if (commandLine.hasOption(CLIManager.QUIET)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> verbose() {
        if (commandLine.hasOption(CLIManager.VERBOSE)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> showErrors() {
        if (commandLine.hasOption(CLIManager.SHOW_ERRORS) || verbose().orElse(false)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> failOnSeverity() {
        if (commandLine.hasOption(CLIManager.FAIL_ON_SEVERITY)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.FAIL_ON_SEVERITY));
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> nonInteractive() {
        if (commandLine.hasOption(CLIManager.NON_INTERACTIVE) || commandLine.hasOption(CLIManager.BATCH_MODE)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> forceInteractive() {
        if (commandLine.hasOption(CLIManager.FORCE_INTERACTIVE)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> altUserSettings() {
        if (commandLine.hasOption(CLIManager.ALTERNATE_USER_SETTINGS)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_USER_SETTINGS));
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> altProjectSettings() {
        if (commandLine.hasOption(CLIManager.ALTERNATE_PROJECT_SETTINGS)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_PROJECT_SETTINGS));
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> altInstallationSettings() {
        if (commandLine.hasOption(CLIManager.ALTERNATE_INSTALLATION_SETTINGS)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_INSTALLATION_SETTINGS));
        }
        if (commandLine.hasOption(CLIManager.ALTERNATE_GLOBAL_SETTINGS)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_GLOBAL_SETTINGS));
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> altUserToolchains() {
        if (commandLine.hasOption(CLIManager.ALTERNATE_USER_TOOLCHAINS)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_USER_TOOLCHAINS));
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> altInstallationToolchains() {
        if (commandLine.hasOption(CLIManager.ALTERNATE_INSTALLATION_TOOLCHAINS)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_INSTALLATION_TOOLCHAINS));
        }
        if (commandLine.hasOption(CLIManager.ALTERNATE_GLOBAL_TOOLCHAINS)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_GLOBAL_TOOLCHAINS));
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> logFile() {
        if (commandLine.hasOption(CLIManager.LOG_FILE)) {
            return Optional.of(commandLine.getOptionValue(CLIManager.LOG_FILE));
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> rawStreams() {
        if (commandLine.hasOption(CLIManager.RAW_STREAMS)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<String> color() {
        if (commandLine.hasOption(CLIManager.COLOR)) {
            if (commandLine.getOptionValue(CLIManager.COLOR) != null) {
                return Optional.of(commandLine.getOptionValue(CLIManager.COLOR));
            } else {
                return Optional.of("auto");
            }
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> offline() {
        if (commandLine.hasOption(CLIManager.OFFLINE)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public Optional<Boolean> help() {
        if (commandLine.hasOption(CLIManager.HELP)) {
            return Optional.of(Boolean.TRUE);
        }
        return Optional.empty();
    }

    @Override
    public void warnAboutDeprecatedOptions(ParserRequest request, Consumer<String> printWriter) {
        if (cliManager.getUsedDeprecatedOptions().isEmpty()) {
            return;
        }
        printWriter.accept("Detected deprecated option use in " + source);
        for (Option option : cliManager.getUsedDeprecatedOptions()) {
            StringBuilder sb = new StringBuilder();
            sb.append("The option ");
            if (option.getOpt() != null) {
                sb.append("-").append(option.getOpt());
            }
            if (option.getLongOpt() != null) {
                if (option.getOpt() != null) {
                    sb.append(",");
                }
                sb.append("--").append(option.getLongOpt());
            }
            sb.append(" is deprecated ");
            if (option.getDeprecated().isForRemoval()) {
                sb.append("and will be removed in a future version");
            }
            if (option.getDeprecated().getSince() != null) {
                sb.append(" since ")
                        .append(request.commandName())
                        .append(" ")
                        .append(option.getDeprecated().getSince());
            }
            printWriter.accept(sb.toString());
        }
    }

    @Override
    public final Options interpolate(UnaryOperator<String> callback) {
        try {
            // now that we have properties, interpolate all arguments
            Interpolator interpolator = createInterpolator();
            CommandLine.Builder commandLineBuilder = CommandLine.builder();
            commandLineBuilder.setDeprecatedHandler(o -> {});
            for (Option option : commandLine.getOptions()) {
                if (!CommonsCliOptions.CLIManager.USER_PROPERTY.equals(option.getOpt())) {
                    List<String> values = option.getValuesList();
                    for (ListIterator<String> it = values.listIterator(); it.hasNext(); ) {
                        it.set(interpolator.interpolate(it.next(), callback));
                    }
                }
                commandLineBuilder.addOption(option);
            }
            for (String arg : commandLine.getArgList()) {
                commandLineBuilder.addArg(interpolator.interpolate(arg, callback));
            }
            return copy(source, cliManager, commandLineBuilder.get());
        } catch (InterpolatorException e) {
            throw new IllegalArgumentException("Could not interpolate CommonsCliOptions", e);
        }
    }

    protected CommonsCliOptions copy(String source, CLIManager cliManager, CommandLine commandLine) {
        return new CommonsCliOptions(source, cliManager, commandLine);
    }

    @Override
    public void displayHelp(ParserRequest request, Consumer<String> printStream) {
        cliManager.displayHelp(request.command(), printStream);
    }

    protected static class CLIManager {
        public static final String USER_PROPERTY = "D";
        public static final String SHOW_VERSION_AND_EXIT = "v";
        public static final String SHOW_VERSION = "V";
        public static final String QUIET = "q";
        public static final String VERBOSE = "X";

        public static final String SHOW_ERRORS = "e";

        public static final String FAIL_ON_SEVERITY = "fos";
        public static final String NON_INTERACTIVE = "non-interactive";
        public static final String BATCH_MODE = "B";
        public static final String FORCE_INTERACTIVE = "force-interactive";
        public static final String ALTERNATE_USER_SETTINGS = "s";
        public static final String ALTERNATE_PROJECT_SETTINGS = "ps";
        public static final String ALTERNATE_INSTALLATION_SETTINGS = "is";
        public static final String ALTERNATE_USER_TOOLCHAINS = "t";
        public static final String ALTERNATE_INSTALLATION_TOOLCHAINS = "it";
        public static final String LOG_FILE = "l";
        public static final String RAW_STREAMS = "raw-streams";
        public static final String COLOR = "color";
        public static final String OFFLINE = "o";
        public static final String HELP = "h";

        // Not an Option: used only for early detection, when CLI args may not be even parsed
        public static final String SHOW_ERRORS_CLI_ARG = "-" + SHOW_ERRORS;

        // parameters handled by script
        public static final String DEBUG = "debug";
        public static final String ENC = "enc";
        public static final String UPGRADE = "up";
        public static final String SHELL = "shell";
        public static final String YJP = "yjp";

        // deprecated ones
        @Deprecated
        public static final String ALTERNATE_GLOBAL_SETTINGS = "gs";

        @Deprecated
        public static final String ALTERNATE_GLOBAL_TOOLCHAINS = "gt";

        protected org.apache.commons.cli.Options options;
        protected final LinkedHashSet<Option> usedDeprecatedOptions = new LinkedHashSet<>();

        @SuppressWarnings("checkstyle:MethodLength")
        protected CLIManager() {
            options = new org.apache.commons.cli.Options();
            prepareOptions(options);
        }

        protected void prepareOptions(org.apache.commons.cli.Options options) {
            options.addOption(Option.builder(HELP)
                    .longOpt("help")
                    .desc("Display help information")
                    .get());
            options.addOption(Option.builder(USER_PROPERTY)
                    .numberOfArgs(2)
                    .valueSeparator('=')
                    .desc("Define a user property")
                    .get());
            options.addOption(Option.builder(SHOW_VERSION_AND_EXIT)
                    .longOpt("version")
                    .desc("Display version information")
                    .get());
            options.addOption(Option.builder(QUIET)
                    .longOpt("quiet")
                    .desc("Quiet output - only show errors")
                    .get());
            options.addOption(Option.builder(VERBOSE)
                    .longOpt("verbose")
                    .desc("Produce execution verbose output")
                    .get());
            options.addOption(Option.builder(SHOW_ERRORS)
                    .longOpt("errors")
                    .desc("Produce execution error messages")
                    .get());
            options.addOption(Option.builder(BATCH_MODE)
                    .longOpt("batch-mode")
                    .desc("Run in non-interactive mode. Alias for --non-interactive (kept for backwards compatability)")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(NON_INTERACTIVE)
                    .desc("Run in non-interactive mode. Alias for --batch-mode")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(FORCE_INTERACTIVE)
                    .desc(
                            "Run in interactive mode. Overrides, if applicable, the CI environment variable and --non-interactive/--batch-mode options")
                    .get());
            options.addOption(Option.builder(ALTERNATE_USER_SETTINGS)
                    .longOpt("settings")
                    .desc("Alternate path for the user settings file")
                    .hasArg()
                    .get());
            options.addOption(Option.builder(ALTERNATE_PROJECT_SETTINGS)
                    .longOpt("project-settings")
                    .desc("Alternate path for the project settings file")
                    .hasArg()
                    .get());
            options.addOption(Option.builder(ALTERNATE_INSTALLATION_SETTINGS)
                    .longOpt("install-settings")
                    .desc("Alternate path for the installation settings file")
                    .hasArg()
                    .get());
            options.addOption(Option.builder(ALTERNATE_USER_TOOLCHAINS)
                    .longOpt("toolchains")
                    .desc("Alternate path for the user toolchains file")
                    .hasArg()
                    .get());
            options.addOption(Option.builder(ALTERNATE_INSTALLATION_TOOLCHAINS)
                    .longOpt("install-toolchains")
                    .desc("Alternate path for the installation toolchains file")
                    .hasArg()
                    .get());
            options.addOption(Option.builder(FAIL_ON_SEVERITY)
                    .longOpt("fail-on-severity")
                    .desc("Configure which severity of logging should cause the build to fail")
                    .hasArg()
                    .get());
            options.addOption(Option.builder(LOG_FILE)
                    .longOpt("log-file")
                    .hasArg()
                    .desc("Log file where all build output will go (disables output color)")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(RAW_STREAMS)
                    .desc("Do not decorate standard output and error streams")
                    .get());
            options.addOption(Option.builder(SHOW_VERSION)
                    .longOpt("show-version")
                    .desc("Display version information WITHOUT stopping build")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(COLOR)
                    .hasArg()
                    .optionalArg(true)
                    .desc("Defines the color mode of the output. Supported are 'auto', 'always', 'never'.")
                    .get());
            options.addOption(Option.builder(OFFLINE)
                    .longOpt("offline")
                    .desc("Work offline")
                    .get());

            // Parameters handled by script
            options.addOption(Option.builder()
                    .longOpt(DEBUG)
                    .desc("Launch the JVM in debug mode (script option).")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(ENC)
                    .desc("Launch the Maven Encryption tool (script option).")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(UPGRADE)
                    .desc("Launch the Maven Upgrade tool (script option).")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(SHELL)
                    .desc("Launch the Maven Shell tool (script option).")
                    .get());
            options.addOption(Option.builder()
                    .longOpt(YJP)
                    .desc("Launch the JVM with Yourkit profiler (script option).")
                    .get());

            // Deprecated
            options.addOption(Option.builder(ALTERNATE_GLOBAL_SETTINGS)
                    .longOpt("global-settings")
                    .desc("<deprecated> Alternate path for the global settings file.")
                    .hasArg()
                    .deprecated(DeprecatedAttributes.builder()
                            .setForRemoval(true)
                            .setSince("4.0.0")
                            .setDescription("Use -is,--install-settings instead.")
                            .get())
                    .get());
            options.addOption(Option.builder(ALTERNATE_GLOBAL_TOOLCHAINS)
                    .longOpt("global-toolchains")
                    .desc("<deprecated> Alternate path for the global toolchains file.")
                    .hasArg()
                    .deprecated(DeprecatedAttributes.builder()
                            .setForRemoval(true)
                            .setSince("4.0.0")
                            .setDescription("Use -it,--install-toolchains instead.")
                            .get())
                    .get());
        }

        public CommandLine parse(String[] args) throws ParseException {
            // We need to eat any quotes surrounding arguments...
            String[] cleanArgs = CleanArgument.cleanArgs(args);
            DefaultParser parser = DefaultParser.builder()
                    .setDeprecatedHandler(this::addDeprecatedOption)
                    .get();
            CommandLine commandLine = parser.parse(options, cleanArgs);
            // to trigger deprecation handler, so we can report deprecation BEFORE we actually use options
            options.getOptions().forEach(commandLine::hasOption);
            return commandLine;
        }

        protected void addDeprecatedOption(Option option) {
            usedDeprecatedOptions.add(option);
        }

        public org.apache.commons.cli.Options getOptions() {
            return options;
        }

        public Set<Option> getUsedDeprecatedOptions() {
            return usedDeprecatedOptions;
        }

        public void displayHelp(String command, Consumer<String> pw) {
            HelpFormatter formatter = new HelpFormatter();

            int width = MessageUtils.getTerminalWidth();
            if (width <= 0) {
                width = HelpFormatter.DEFAULT_WIDTH;
            }

            pw.accept("");

            StringWriter sw = new StringWriter();
            PrintWriter pw2 = new PrintWriter(sw);
            formatter.printHelp(
                    pw2,
                    width,
                    commandLineSyntax(command),
                    System.lineSeparator() + "Options:",
                    options,
                    HelpFormatter.DEFAULT_LEFT_PAD,
                    HelpFormatter.DEFAULT_DESC_PAD,
                    System.lineSeparator(),
                    false);
            pw2.flush();
            for (String s : sw.toString().split(System.lineSeparator())) {
                pw.accept(s);
            }
        }

        protected String commandLineSyntax(String command) {
            return command + " [options] [goals]";
        }
    }
}