SubCommandSetImpl.java

/*
 * Copyright (c) 2016, Stein Eldar Johnsen
 *
 * 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 net.morimekta.terminal.args.impl;

import net.morimekta.collect.UnmodifiableList;
import net.morimekta.collect.UnmodifiableMap;
import net.morimekta.terminal.args.Arg;
import net.morimekta.terminal.args.ArgException;
import net.morimekta.terminal.args.ArgParser;
import net.morimekta.terminal.args.Option;
import net.morimekta.terminal.args.SubCommand;
import net.morimekta.terminal.args.SubCommandSet;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
 * The argument part of the sub-command. The sub-command set is
 * a collection of sub-commands that react to CLI arguments. It will
 * <b>always</b> trigger (and throw {@link ArgException} if
 * not valid), so the sub-command <b>must</b> be added last.
 *
 * @param <SubCommandDef> The sub-command interface.
 */
public class SubCommandSetImpl<SubCommandDef>
        extends BaseArg
        implements SubCommandSet<SubCommandDef> {
    /**
     * @return If a subcommand has been selected.
     */
    public boolean isSelected() {
        return selected != null;
    }

    // --- SubCommandSet ---
    @Override
    public List<SubCommand<? extends SubCommandDef>> getSubCommands() {
        return subCommands;
    }

    @Override
    public SubCommand<? extends SubCommandDef> getSubCommandByName(String name) {
        var wrapper = subCommandMap.get(name);
        if (wrapper != null) {
            return wrapper.subCommand;
        }
        return null;
    }

    @Override
    public ArgParser parserForSubCommand(String name) {
        var cmd = subCommandMap.get(name);
        if (cmd == null) {
            throw new ArgException(parent, "Unknown sub-command %s.", name);
        }
        return cmd.parser;
    }

    // --- Arg ---

    @Override
    public String getSingleLineUsage() {
        StringBuilder sb = new StringBuilder();
        if (!isRequired()) {
            sb.append('[');
        }
        sb.append(getName());
        sb.append(" [...]");
        if (!isRequired()) {
            sb.append(']');
        }

        return sb.toString();
    }

    @Override
    public String getPrefix() {
        return getName();
    }

    @Override
    public void validate() throws ArgException {
        if (selected != null) {
            selected.parser.validate();
        } else if (isRequired()) {
            throw new ArgException("%s not chosen", getName());
        }
    }

    @Override
    public int apply(List<String> args) {
        if (selected != null) {
            throw new ArgException("%s already selected", getName());
        }

        String name = args.get(0);
        selected = subCommandMap.get(name);
        if (selected == null) {
            throw new ArgException("No such %s: %s", getName(), name);
        }
        // Skip the sub-command name itself, and parse the remaining args
        // in the sub-command argument argumentParser.
        List<String> subArgs = args.subList(1, args.size());
        selected.parser.parse(subArgs);
        consumer.accept(selected.instance);

        return args.size();
    }

    // --- Object ---

    @Override
    public String toString() {
        return "SubCommandSet{" + getName() + ", " + subCommands + "}";
    }

    public static class BuilderImpl<SubCommandDef>
            extends ArgParserBuilderImpl
            implements SubCommandSet.Builder<SubCommandDef> {
        public BuilderImpl(
                String subCommandsName,
                String subCommandsUsage,
                Consumer<SubCommandDef> subCommandConsumer,
                ArgParserImpl parent,
                String program,
                String version,
                String description,
                List<Option> options,
                List<Arg> arguments,
                Map<String, Option> longNameOptions,
                Map<Character, Option> shortOptions) {
            super(parent,
                  program,
                  version,
                  description,
                  options,
                  arguments,
                  longNameOptions,
                  shortOptions);
            this.subCommandsName = subCommandsName;
            this.subCommandsUsage = subCommandsUsage;
            this.subCommandConsumer = subCommandConsumer;
            this.subCommands = new ArrayList<>();
            this.subCommandMap = new HashMap<>();
            this.requiredSubCommand = true;

            this.parentForSubCommands = new ArgParserImpl(
                    parent,
                    program,
                    version,
                    description,
                    options,
                    arguments,
                    longNameOptions,
                    shortOptions,
                    null);
        }

        @Override
        public ArgParser build() {
            if (subCommands.isEmpty()) {
                throw new IllegalStateException("No sub-commands added on sub-command set.");
            }
            var scs = new SubCommandSetImpl<>(
                    // same argument parser, but without the sub commands.
                    buildInternal(arguments, null),
                    subCommandsName,
                    subCommandsUsage,
                    subCommandConsumer,
                    defaultSubCommand,
                    requiredSubCommand,
                    subCommands,
                    subCommandMap);
            return buildInternal(arguments, scs);
        }

        private ArgParserImpl buildInternal(List<Arg> args, SubCommandSetImpl<?> subCommandSet) {
            return new ArgParserImpl(
                    parent,
                    program,
                    version,
                    description,
                    options,
                    args,
                    longNameOptions,
                    shortOptions,
                    subCommandSet);
        }

        @Override
        public BuilderImpl<SubCommandDef> optionalCommand() {
            this.requiredSubCommand = false;
            return this;
        }

        @Override
        public BuilderImpl<SubCommandDef> defaultCommand(String name) {
            if (!subCommandMap.containsKey(name)) {
                throw new IllegalArgumentException("SubCommand with name " + name + " does not exist.");
            }
            if (defaultSubCommand != null) {
                throw new IllegalStateException("Default SubCommand already set for " + name + ".");
            }
            this.requiredSubCommand = false;
            this.defaultSubCommand = name;
            return this;
        }

        @Override
        public BuilderImpl<SubCommandDef> add(SubCommand<? extends SubCommandDef> subCommand) {
            // check that the command can instantiate.
            var builder = new ArgParserImpl.BuilderImpl(
                    parentForSubCommands,
                    program + " " + subCommand.getName(),
                    version,
                    description);
            var instance = subCommand.newInstance(builder);
            var wrapper = new SubCommandHolder<>(subCommand, instance, builder.build());

            if (subCommandMap.containsKey(subCommand.getName())) {
                throw new IllegalArgumentException("SubCommand with name " + subCommand.getName() + " already exists.");
            }
            this.subCommands.add(subCommand);
            this.subCommandMap.put(subCommand.getName(), wrapper);
            for (String alias : subCommand.getAliases()) {
                if (subCommandMap.containsKey(alias)) {
                    throw new IllegalArgumentException("SubCommand (" + subCommand.getName() + ") alias " + alias + " already exists");
                }
                this.subCommandMap.put(alias, wrapper);
            }
            return this;
        }

        private final ArgParserImpl parentForSubCommands;

        // SubCommands
        private final String                  subCommandsName;
        private final String                  subCommandsUsage;
        private final Consumer<SubCommandDef> subCommandConsumer;

        private boolean requiredSubCommand;
        private String  defaultSubCommand;

        private final List<SubCommand<? extends SubCommandDef>>    subCommands;
        private final Map<String, SubCommandHolder<SubCommandDef>> subCommandMap;
    }

    private static class SubCommandHolder<SubCommandDef> {
        private final SubCommand<? extends SubCommandDef> subCommand;
        private final SubCommandDef                       instance;
        private final ArgParser                           parser;

        private SubCommandHolder(SubCommand<? extends SubCommandDef> subCommand,
                                 SubCommandDef instance,
                                 ArgParser parser) {
            this.subCommand = subCommand;
            this.instance = instance;
            this.parser = parser;
        }
    }

    private SubCommandSetImpl(ArgParserImpl parent,
                              String name,
                              String usage,
                              Consumer<SubCommandDef> consumer,
                              String defaultValue,
                              boolean required,
                              List<SubCommand<? extends SubCommandDef>> subCommands,
                              Map<String, SubCommandHolder<SubCommandDef>> subCommandMap) {
        super(name, usage, defaultValue, false, required, false);

        this.parent = parent;
        this.subCommands = UnmodifiableList.asList(subCommands);
        this.subCommandMap = UnmodifiableMap.asMap(subCommandMap);
        this.consumer = consumer;
    }

    private final List<SubCommand<? extends SubCommandDef>>    subCommands;
    private final Map<String, SubCommandHolder<SubCommandDef>> subCommandMap;
    private final Consumer<SubCommandDef>                      consumer;

    private final ArgParserImpl parent;

    private SubCommandHolder<SubCommandDef> selected;
}