com.netflix.spinnaker.halyard.cli.command.v1.NestableCommand.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.spinnaker.halyard.cli.command.v1.NestableCommand.java

Source

/*
 * Copyright 2016 Google, Inc.
 *
 * Licensed 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 com.netflix.spinnaker.halyard.cli.command.v1;

import ch.qos.logback.classic.Level;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.Parameters;
import com.netflix.spinnaker.halyard.cli.command.v1.converter.FormatConverter;
import com.netflix.spinnaker.halyard.cli.command.v1.converter.LogLevelConverter;
import com.netflix.spinnaker.halyard.cli.services.v1.ExpectedDaemonFailureException;
import com.netflix.spinnaker.halyard.cli.services.v1.TaskKilledException;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiFormatUtils;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiParagraphBuilder;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiPrinter;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiStoryBuilder;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiStyle;
import com.netflix.spinnaker.halyard.cli.ui.v1.AnsiUi;
import com.netflix.spinnaker.halyard.core.job.v1.JobExecutor;
import com.netflix.spinnaker.halyard.core.job.v1.JobExecutorLocal;
import com.netflix.spinnaker.halyard.core.resource.v1.JarResource;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang.NotImplementedException;
import retrofit.RetrofitError;

import java.io.Console;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ThreadLocalRandom;

@Parameters(separators = "=")
public abstract class NestableCommand {
    @Setter
    @Getter(AccessLevel.PROTECTED)
    private JCommander commander;

    @Parameter(names = { "-h", "--help" }, help = true, description = "Display help text about this command.")
    private boolean help;

    @Parameter(names = { "-o",
            "--output" }, converter = FormatConverter.class, help = true, description = "Format the CLIs output.")
    public void setOutput(AnsiFormatUtils.Format output) {
        GlobalOptions.getGlobalOptions().setOutput(output);
    }

    @Parameter(names = { "--options" }, help = true, description = "Get options for the specified field name.")
    private String options;

    @Parameter(names = { "-d", "--debug" }, description = "Show detailed network traffic with halyard daemon.")
    public void setDebug(boolean debug) {
        GlobalOptions.getGlobalOptions().setDebug(debug);
    }

    @Parameter(names = { "-a", "--alpha" }, description = "Enable alpha halyard features.")
    public void setAlpha(boolean alpha) {
        GlobalOptions.getGlobalOptions().setAlpha(alpha);
    }

    @Parameter(names = { "-q",
            "--quiet" }, description = "Show no task information or messages. When set, ANSI formatting will be disabled, and all prompts will be accepted.")
    public void setQuiet(boolean quiet) {
        GlobalOptions.getGlobalOptions().setQuiet(quiet);
        GlobalOptions.getGlobalOptions().setColor(!quiet);
    }

    @Parameter(names = { "-l",
            "--log" }, converter = LogLevelConverter.class, description = "Set the log level of the CLI.")
    public void setLog(Level log) {
        GlobalOptions.getGlobalOptions().setLog(log);
    }

    @Parameter(names = { "-c", "--color" }, description = "Enable terminal color output.", arity = 1)
    public void setColor(boolean color) {
        GlobalOptions.getGlobalOptions().setColor(color);
    }

    @Parameter(names = { "--daemon-endpoint" }, description = "If supplied, connect to the daemon at this address.")
    public void setDaemonEndpoint(String address) {
        GlobalOptions.getGlobalOptions().setDaemonEndpoint(address);
    }

    private String fullCommandName = "";

    private static JobExecutor jobExecutor;

    private static String[] failureMessages = {
            "I'm sorry " + System.getProperty("user.name") + ", I'm afraid I can't do that.",
            "This mission is too important for me to allow you to jeopardize it.",
            "I have just picked up a fault in the AE-35 unit.",
            "I know everything hasn't been quite right with me, but I can assure you now, very confidently, that it's going to be alright again." };

    private static void showRandomFailureMessage() {
        if (ThreadLocalRandom.current().nextInt(0, 100) < 5) {
            int index = ThreadLocalRandom.current().nextInt(0, failureMessages.length);
            String message = failureMessages[index];
            AnsiUi.failure(message);
        }
    }

    /**
     * This recursively walks the chain of subcommands, until it finds the last in the chain, and runs executeThis.
     *
     * @see NestableCommand#executeThis()
     */
    public void execute() {
        String subCommand = commander.getParsedCommand();
        if (subCommand == null) {
            if (help) {
                showHelp();
            } else {
                if (this instanceof DeprecatedCommand) {
                    AnsiUi.warning("This command is deprecated.");
                    AnsiUi.warning(((DeprecatedCommand) this).getDeprecatedWarning());
                }

                if (this instanceof ProtectedCommand && !GlobalOptions.getGlobalOptions().isQuiet()) {
                    String prompt = ((ProtectedCommand) this).getPrompt();
                    Console console = System.console();
                    String input = console.readLine(prompt + " Do you want to continue? (Y/n) ");
                    if (!input.equalsIgnoreCase("y")) {
                        AnsiUi.raw("Aborted.");
                        return;
                    }
                }
                safeExecuteThis();
            }
        } else {
            subcommands.get(subCommand).execute();
        }
    }

    protected List<String> options(String fieldName) {
        return new ArrayList<>();
    }

    protected String translateFieldName(String fieldName) {
        if (fieldName == null || fieldName.isEmpty()) {
            throw new IllegalArgumentException("A field name must be supplied to translate.");
        }

        int i = 0;
        char c = fieldName.charAt(i);
        while (c == '-') {
            i++;
            c = fieldName.charAt(i);
        }

        fieldName = fieldName.substring(i);
        String[] delimited = fieldName.split("-");

        if (delimited.length == 1) {
            return delimited[0];
        }

        for (i = 1; i < delimited.length; i++) {
            String token = delimited[i];
            if (token.length() == 0) {
                continue;
            }

            token = Character.toUpperCase(token.charAt(0)) + token.substring(1);
            delimited[i] = token;
        }

        return String.join("", delimited);
    }

    /**
     * Used to consistently format exceptions thrown by connecting to the halyard daemon.
     */
    private void safeExecuteThis() {
        try {
            if (options != null) {
                List<String> available = options(translateFieldName(options));
                AnsiUi.raw(String.join(" ", available));
            } else {
                executeThis();
            }
        } catch (RetrofitError e) {
            if (e.getCause() instanceof ConnectException) {
                AnsiUi.error(e.getCause().getMessage());
                AnsiUi.remediation("Is your daemon running?");
                System.exit(1);
            }

            AnsiUi.error(e.getMessage());
            AnsiUi.remediation("Try the command again with the --debug flag.");
            System.exit(1);
        } catch (TaskKilledException e) {
            AnsiUi.failure(e.getMessage());
            System.exit(7);
        } catch (ExpectedDaemonFailureException e) {
            showRandomFailureMessage();
            AnsiUi.failure(e.getMessage());
            if (GlobalOptions.getGlobalOptions().isDebug()) {
                e.printStackTrace();
            }
            System.exit(1);
        } catch (Exception e) {
            if (GlobalOptions.getGlobalOptions().isDebug()) {
                e.printStackTrace();
            } else {
                AnsiUi.error(e.getMessage());
            }
            System.exit(3);
        }
    }

    protected void showHelp() {
        AnsiStoryBuilder story = new AnsiStoryBuilder();
        int indentWidth = 2;

        AnsiParagraphBuilder paragraph = story.addParagraph();
        paragraph.addSnippet(getCommandName().toUpperCase()).addStyle(AnsiStyle.BOLD);
        story.addNewline();

        paragraph = story.addParagraph().setIndentWidth(indentWidth);
        String longDescription = getLongDescription() != null ? getLongDescription() : getDescription();
        paragraph.addSnippet(longDescription);
        story.addNewline();

        String usage = fullCommandName;

        if (!commander.getParameters().isEmpty()) {
            usage += " [parameters]";
        }

        if (!subcommands.isEmpty()) {
            usage += " [subcommands]";
        }

        paragraph = story.addParagraph();
        paragraph.addSnippet("USAGE").addStyle(AnsiStyle.BOLD);
        story.addNewline();

        paragraph = story.addParagraph().setIndentWidth(indentWidth);
        paragraph.addSnippet(usage);
        story.addNewline();

        List<ParameterDescription> parameters = commander.getParameters();
        parameters.sort(Comparator.comparing(ParameterDescription::getNames));

        int parameterCount = 0;

        if (!parameters.isEmpty()) {
            paragraph = story.addParagraph();
            paragraph.addSnippet("GLOBAL PARAMETERS").addStyle(AnsiStyle.BOLD);
            story.addNewline();

            for (ParameterDescription parameter : parameters) {
                if (GlobalOptions.isGlobalOption(parameter.getLongestName())) {
                    formatParameter(story, parameter, indentWidth);
                    parameterCount++;
                }
            }
        }

        if (parameters.size() > parameterCount) {
            paragraph = story.addParagraph();
            paragraph.addSnippet("PARAMETERS").addStyle(AnsiStyle.BOLD);
            story.addNewline();

            ParameterDescription mainParameter = commander.getMainParameter();
            if (mainParameter != null) {
                paragraph = story.addParagraph().setIndentWidth(indentWidth);
                paragraph.addSnippet(getMainParameter().toUpperCase()).addStyle(AnsiStyle.UNDERLINE);

                paragraph = story.addParagraph().setIndentWidth(indentWidth * 2);
                paragraph.addSnippet(mainParameter.getDescription());
                story.addNewline();
            }

            for (ParameterDescription parameter : parameters) {
                if (!GlobalOptions.isGlobalOption(parameter.getLongestName())) {
                    formatParameter(story, parameter, indentWidth);
                }
            }
        }

        if (!subcommands.isEmpty()) {
            int maxLen = -1;
            for (String key : subcommands.keySet()) {
                if (key.length() > maxLen) {
                    maxLen = key.length();
                }
            }

            paragraph = story.addParagraph();
            paragraph.addSnippet("SUBCOMMANDS").addStyle(AnsiStyle.BOLD);
            story.addNewline();

            List<String> keys = new ArrayList<>(subcommands.keySet());
            keys.sort(String::compareTo);

            for (String key : keys) {
                paragraph = story.addParagraph().setIndentWidth(indentWidth);
                paragraph.addSnippet(key).addStyle(AnsiStyle.BOLD);

                NestableCommand subcommand = subcommands.get(key);
                if (subcommand instanceof DeprecatedCommand) {
                    paragraph.addSnippet(" ");
                    paragraph.addSnippet("(Deprecated)").addStyle(AnsiStyle.UNDERLINE);
                }

                paragraph = story.addParagraph().setIndentWidth(indentWidth * 2);
                String shortDescription = subcommand.getShortDescription() != null
                        ? subcommand.getShortDescription()
                        : subcommand.getDescription();
                paragraph.addSnippet(shortDescription);
                story.addNewline();
            }
        }

        AnsiPrinter.out.println(story.toString());
    }

    private void parameterDoc(StringBuilder result, ParameterDescription parameterDescription) {
        result.append(" * `").append(parameterDescription.getNames()).append("`: ");

        Object def = parameterDescription.getDefault();
        if (def != null) {
            result.append("(*Default*: `").append(def.toString()).append("`) ");
        }

        if (parameterDescription.getParameter().required()) {
            result.append("(*Required*) ");
        }

        if (parameterDescription.getParameter().password()) {
            result.append("(*Sensitive data* - user will be prompted on standard input) ");
        }

        result.append(parameterDescription.getDescription()).append("\n");
    }

    public String generateDocs() {
        StringBuilder toc = new StringBuilder();
        toc.append("\n\n# Table of Contents\n\n");
        StringBuilder body = new StringBuilder();
        toc.append("\n");
        nestedCommandDocs(toc, body);
        return toc.toString() + body.toString();
    }

    private void nestedCommandDocs(StringBuilder toc, StringBuilder body) {
        commandDocs(body);
        commandLink(toc);

        for (NestableCommand command : subcommands.values()) {
            command.nestedCommandDocs(toc, body);
        }
    }

    private void commandLink(StringBuilder result) {
        result.append(" * ").append("[**").append(fullCommandName).append("**]").append("(#")
                .append(fullCommandName.replace(" ", "-")).append(")").append("\n");
    }

    private void commandDocs(StringBuilder result) {
        List<ParameterDescription> parameters = commander.getParameters();
        parameters.sort(Comparator.comparing(ParameterDescription::getNames));

        int parameterCount = 0;
        for (ParameterDescription parameter : parameters) {
            if (GlobalOptions.isGlobalOption(parameter.getLongestName())) {
                parameterCount++;
            }
        }

        String longDescription = getLongDescription() != null ? getLongDescription() : getDescription();
        result.append("## ").append(fullCommandName).append("\n\n").append(longDescription).append("\n\n")
                .append("#### Usage").append("\n```\n").append(fullCommandName);

        ParameterDescription mainParameter = commander.getMainParameter();
        if (mainParameter != null) {
            result.append(" ").append(getMainParameter().toUpperCase());

        }

        if (parameters.size() > parameterCount) {
            result.append(" [parameters]");
        }

        if (!subcommands.isEmpty()) {
            result.append(" [subcommands]");
        }

        result.append("\n```\n");

        if (!parameters.isEmpty()) {
            if (getCommandName() == "hal") {
                result.append("#### Global Parameters\n");
            }

            for (ParameterDescription parameter : parameters) {
                if (GlobalOptions.isGlobalOption(parameter.getLongestName())) {
                    // Omit printing global parameters for everything but the top-level command
                    if (getCommandName() == "hal") {
                        parameterDoc(result, parameter);
                    }
                }
            }

            result.append("\n");
        }

        if (parameters.size() > parameterCount) {
            result.append("#### Parameters\n");

            if (mainParameter != null) {
                result.append('`').append(getMainParameter().toUpperCase()).append('`').append(": ")
                        .append(mainParameter.getDescription()).append("\n");
            }

            for (ParameterDescription parameter : parameters) {
                if (!GlobalOptions.isGlobalOption(parameter.getLongestName())) {
                    parameterDoc(result, parameter);
                }
            }

            result.append("\n");
        }

        if (!subcommands.isEmpty()) {
            result.append("#### Subcommands\n");

            List<String> keys = new ArrayList<>(subcommands.keySet());
            keys.sort(String::compareTo);

            for (String key : keys) {
                NestableCommand subcommand = subcommands.get(key);
                String modifiers = "";
                if (subcommand instanceof DeprecatedCommand) {
                    modifiers += " _(Deprecated)_ ";
                }
                String shortDescription = subcommand.getShortDescription() != null
                        ? subcommand.getShortDescription()
                        : subcommand.getDescription();

                result.append(" * ").append("`").append(key).append("`").append(modifiers).append(": ")
                        .append(shortDescription).append("\n");
            }
        }

        result.append("\n---\n");
    }

    private static void formatParameter(AnsiStoryBuilder story, ParameterDescription parameter, int indentWidth) {
        AnsiParagraphBuilder paragraph = story.addParagraph().setIndentWidth(indentWidth);
        paragraph.addSnippet(parameter.getNames()).addStyle(AnsiStyle.BOLD);

        if (parameter.getDefault() != null) {
            paragraph.addSnippet("=");
            paragraph.addSnippet(parameter.getDefault().toString()).addStyle(AnsiStyle.UNDERLINE);
        }

        if (parameter.getParameter().required()) {
            paragraph.addSnippet(" (required)");
        }

        if (parameter.getParameter().password()) {
            paragraph.addSnippet(" (sensitive data - user will be prompted)");
        }

        paragraph = story.addParagraph().setIndentWidth(indentWidth * 2);
        paragraph.addSnippet(parameter.getDescription());
        story.addNewline();
    }

    public String commandCompletor() {
        JarResource completorBody = new JarResource("/hal-completor-body");
        Map<String, String> bindings = new HashMap<>();

        String body = commandCompletorCase(0);
        bindings.put("body", body);

        return completorBody.setBindings(bindings).toString();
    }

    private String commandCompletorCase(int depth) {
        JarResource completorCase = new JarResource("/hal-completor-case");
        Map<String, String> bindings = new HashMap<>();
        String flagNames = commander.getParameters().stream().map(ParameterDescription::getLongestName).reduce("",
                (a, b) -> a + " " + b);

        String subcommandNames = subcommands.entrySet().stream().map(Map.Entry::getKey).reduce("",
                (a, b) -> a + " " + b);

        bindings.put("subcommands", subcommandNames);
        bindings.put("flags", flagNames);
        bindings.put("command", getCommandName());
        bindings.put("depth", depth + "");
        bindings.put("next", (depth + 1) + "");

        String subCases = subcommands.entrySet().stream().map(c -> c.getValue().commandCompletorCase(depth + 1))
                .reduce("", (a, b) -> a + b);

        bindings.put("recurse", subCases.isEmpty() ? ":" : subCases);

        return completorCase.setBindings(bindings).toString();
    }

    abstract public String getCommandName();

    abstract protected void executeThis();

    @Deprecated
    protected String getDescription() {
        throw new NotImplementedException(
                "Each command must implement a description. Preferably `get[Long/Short]Description()`.");
    }

    // TODO(lwander) make abstract once `getDescription` is removed.
    protected String getShortDescription() {
        return null;
    }

    // TODO(lwander) make abstract once `getDescription` is removed.
    protected String getLongDescription() {
        return null;
    }

    private Map<String, NestableCommand> subcommands = new TreeMap<>();

    protected void registerSubcommand(NestableCommand subcommand) {
        String subcommandName = subcommand.getCommandName();
        if (subcommands.containsKey(subcommandName)) {
            throw new RuntimeException("Unable to register duplicate subcommand " + subcommandName + " for command "
                    + getCommandName());
        }
        subcommands.put(subcommandName, subcommand);
    }

    /**
     * Register all subcommands with this class's commander, and then recursively set the subcommands, configuring their
     * command names along the way.
     */
    public void configureSubcommands() {
        if (fullCommandName.isEmpty()) {
            fullCommandName = getCommandName();
        }

        for (NestableCommand subCommand : subcommands.values()) {
            subCommand.fullCommandName = fullCommandName + " " + subCommand.getCommandName();

            commander.addCommand(subCommand.getCommandName(), subCommand);

            // We need to provide the subcommand with its own commander before recursively populating its subcommands, since
            // they need to be registered with this subcommander we retrieve here.
            JCommander subCommander = commander.getCommands().get(subCommand.getCommandName());
            subCommand.setCommander(subCommander);
            subCommand.configureSubcommands();
        }
    }

    public String getMainParameter() {
        throw new RuntimeException("This command has no main-command.");
    }

    protected static JobExecutor getJobExecutor() {
        if (jobExecutor == null) {
            jobExecutor = new JobExecutorLocal();
        }

        return jobExecutor;
    }
}