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;
}