/**
* PermissionsEx
* Copyright (C) zml and PermissionsEx contributors
*
* 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 ninja.leaping.permissionsex.util.command.args;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import ninja.leaping.permissionsex.util.GuavaCollectors;
import ninja.leaping.permissionsex.util.GuavaStartsWithPredicate;
import ninja.leaping.permissionsex.util.StartsWithPredicate;
import ninja.leaping.permissionsex.util.Translatable;
import ninja.leaping.permissionsex.util.Translations;
import ninja.leaping.permissionsex.util.command.CommandContext;
import ninja.leaping.permissionsex.util.command.Commander;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import static ninja.leaping.permissionsex.util.Translations.t;
/**
* Class containing factory methods to combine single-value command elements
*/
public class GenericArguments {
private GenericArguments() {}
/**
* Expects no arguments
*
* @return An expectation of no arguments
*/
public static CommandElement none() {
return new SequenceCommandElement(ImmutableList.<CommandElement>of());
}
private static CommandElement markTrue(String flag) {
return new MarkTrueCommandElement(flag);
}
private static class MarkTrueCommandElement extends CommandElement {
public MarkTrueCommandElement(String flag) {
super(Translations.untr(flag));
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return true;
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
return Collections.emptyList();
}
}
public static FlagCommandElementBuilder flags() {
return new FlagCommandElementBuilder();
}
public enum UnknownFlagBehavior {
/**
* Throw an {@link ArgumentParseException} when an unknown flag is encountered
*/
ERROR,
/**
* Mark the flag as a non-value flag
*/
ACCEPT_NONVALUE,
/**
* Accept the flag with a string-typed value
*/
ACCEPT_VALUE,
/**
* Act as if the unknown flag is an ordinary argument
*/
IGNORE
}
public static class FlagCommandElementBuilder {
private final Map<List<String>, CommandElement> usageFlags = new HashMap<>();
private final Map<String, CommandElement> shortFlags = new HashMap<>();
private final Map<String, CommandElement> longFlags = new HashMap<>();
private UnknownFlagBehavior unknownLongFlagBehavior = UnknownFlagBehavior.ERROR;
private UnknownFlagBehavior unknownShortFlagBehavior = UnknownFlagBehavior.ERROR;
private boolean anchorFlags = false;
public FlagCommandElementBuilder flag(String... specs) {
final List<String> availableFlags = new ArrayList<>(specs.length);
CommandElement el = null;
for (String spec : specs) {
if (spec.startsWith("-")) {
final String flagKey = spec.substring(1);
if (el == null) {
el = markTrue(flagKey);
}
availableFlags.add(flagKey);
longFlags.put(flagKey.toLowerCase(), el);
} else {
for (int i = 0; i < spec.length(); ++i) {
final String flagKey = spec.substring(i, i + 1);
if (el == null) {
el = markTrue(flagKey);
}
availableFlags.add(flagKey);
shortFlags.put(flagKey, el);
}
}
}
usageFlags.put(availableFlags, el);
return this;
}
public FlagCommandElementBuilder valueFlag(CommandElement value, String... specs) {
final List<String> availableFlags = new ArrayList<>(specs.length);
String valueStore = null;
for (String spec : specs) {
if (spec.startsWith("-")) {
availableFlags.add(spec);
final String flagKey = spec.substring(1);
if (valueStore == null) {
valueStore = flagKey;
}
longFlags.put(flagKey.toLowerCase(), value);
} else {
for (int i = 0; i < spec.length(); ++i) {
final String flagKey = spec.substring(i, i + 1);
if (valueStore == null) {
valueStore = flagKey;
}
availableFlags.add(flagKey);
shortFlags.put(flagKey, value);
}
}
}
usageFlags.put(availableFlags, markTrue(valueStore));
return this;
}
/**
* If this is true, any long flag (--) will be accepted and added as a flag
*
* @param behavior The behavior upon encountering an unknown long flag
* @return this
*/
public FlagCommandElementBuilder setUnknownLongFlagBehavior(UnknownFlagBehavior behavior) {
this.unknownLongFlagBehavior = behavior;
return this;
}
public FlagCommandElementBuilder setUnknownShortFlagBehavior(UnknownFlagBehavior behavior) {
this.unknownShortFlagBehavior = behavior;
return this;
}
/**
* Whether flags should be anchored to the beginning of the text (so flags will
* only be picked up if they are at the beginning of the input)
* @param anchorFlags Whether flags are anchored
* @return this
*/
public FlagCommandElementBuilder setAnchorFlags(boolean anchorFlags) {
this.anchorFlags = anchorFlags;
return this;
}
public CommandElement buildWith(CommandElement wrapped) {
return new FlagCommandElement(wrapped, usageFlags, shortFlags, longFlags, unknownShortFlagBehavior, unknownLongFlagBehavior, anchorFlags);
}
}
private static class FlagCommandElement extends CommandElement {
private final CommandElement childElement;
private final Map<List<String>, CommandElement> usageFlags;
private final Map<String, CommandElement> shortFlags;
private final Map<String, CommandElement> longFlags;
private final UnknownFlagBehavior unknownShortFlagBehavior;
private final UnknownFlagBehavior unknownLongFlagBehavior;
private final boolean anchorFlags;
protected FlagCommandElement(CommandElement childElement, Map<List<String>, CommandElement> usageFlags, Map<String, CommandElement> shortFlags, Map<String, CommandElement> longFlags, UnknownFlagBehavior unknownShortFlagBehavior, UnknownFlagBehavior unknownLongFlagBehavior, boolean anchorFlags) {
super(null);
this.childElement = childElement;
this.usageFlags = usageFlags;
this.shortFlags = shortFlags;
this.longFlags = longFlags;
this.unknownShortFlagBehavior = unknownShortFlagBehavior;
this.unknownLongFlagBehavior = unknownLongFlagBehavior;
this.anchorFlags = anchorFlags;
}
@Override
public void parse(CommandArgs args, CommandContext context) throws ArgumentParseException {
int startIdx = args.getPosition();
String arg;
while (args.hasNext()) {
arg = args.next();
if (arg.startsWith("-")) {
int flagStartIdx = args.getPosition();
boolean ignored;
if (arg.startsWith("--")) { // Long flag
String longFlag = arg.substring(2);
ignored = !parseLongFlag(longFlag, args, context);
} else {
arg = arg.substring(1);
ignored = !parseShortFlags(arg, args, context);
}
if (!ignored) {
args.removeArgs(flagStartIdx, args.getPosition());
}
} else if (this.anchorFlags) {
break;
}
}
args.setPosition(startIdx);
if (childElement != null) {
childElement.parse(args, context);
}
}
private boolean parseLongFlag(String longFlag, CommandArgs args, CommandContext context) throws ArgumentParseException {
if (longFlag.isEmpty()) {
return false;
}
if (longFlag.contains("=")) {
final String[] flagSplit = longFlag.split("=", 2);
longFlag = flagSplit[0];
String value = flagSplit[1];
CommandElement element = longFlags.get(longFlag.toLowerCase());
if (element == null) {
switch (unknownLongFlagBehavior) {
case ERROR:
throw args.createError(t("Unknown long flag %s specified", args));
case ACCEPT_NONVALUE:
context.putArg(longFlag, value);
break;
case IGNORE:
return false;
}
} else {
args.insertArg(value);
element.parse(args, context);
}
} else {
CommandElement element = longFlags.get(longFlag.toLowerCase());
if (element == null) {
switch (unknownLongFlagBehavior) {
case ERROR:
throw args.createError(t("Unknown long flag %s specified", args));
case ACCEPT_NONVALUE:
context.putArg(longFlag, true);
break;
case IGNORE:
return false;
}
context.putArg(longFlag, true);
} else {
element.parse(args, context);
}
}
return true;
}
private boolean parseShortFlags(String shortFlags, CommandArgs args, CommandContext context) throws ArgumentParseException {
if (shortFlags.isEmpty()) {
return false;
}
for (int i = 0; i < shortFlags.length(); ++i) {
final String flagChar = shortFlags.substring(i, i + 1);
CommandElement element = this.shortFlags.get(flagChar);
if (element == null) {
switch (unknownShortFlagBehavior) {
case IGNORE:
if (i == 0) {
return false;
} // fall-through
case ERROR:
throw args.createError(t("Unknown short flag %s specified", flagChar));
case ACCEPT_NONVALUE:
context.putArg(flagChar, true);
}
} else {
element.parse(args, context);
}
}
return true;
}
@Override
public <TextType> TextType getUsage(Commander<TextType> src) {
final List<Object> builder = new ArrayList<>();
for (Map.Entry<List<String>, CommandElement> arg : usageFlags.entrySet()) {
builder.add("[");
for (Iterator<String> it = arg.getKey().iterator(); it.hasNext();) {
builder.add("-");
builder.add(it.next());
if (it.hasNext()) {
builder.add("|");
}
}
if (!(arg.getValue() instanceof MarkTrueCommandElement)) { // true flag
builder.add(" ");
builder.add(arg.getValue().getUsage(src));
}
builder.add("]");
builder.add(" ");
}
if (childElement != null) {
builder.add(childElement.getUsage(src));
}
return src.fmt().combined(builder.toArray());
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return null;
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
int startIdx = args.getPosition();
Optional<String> arg;
while (args.hasNext()) {
arg = args.nextIfPresent();
if (arg.get().startsWith("-")) {
int flagStartIdx = args.getPosition();
if (arg.get().startsWith("--")) { // Long flag
String longFlag = arg.get().substring(2);
List<String> ret = tabCompleteLongFlag(longFlag, src, args, context);
if (ret != null) {
return ret;
}
} else {
final String argStr = arg.get().substring(1);
List<String> ret = tabCompleteShortFlags(argStr, src, args, context);
if (ret != null) {
return ret;
}
}
args.removeArgs(flagStartIdx, args.getPosition());
} else if (this.anchorFlags) {
break;
}
}
args.setPosition(startIdx);
if (childElement != null) {
return childElement.tabComplete(src, args, context);
} else {
return Collections.emptyList();
}
}
private <TextType> List<String> tabCompleteLongFlag(String longFlag, Commander<TextType> src, CommandArgs args, CommandContext context) {
if (longFlag.isEmpty()) {
return null;
}
if (longFlag.contains("=")) {
final String[] flagSplit = longFlag.split("=", 2);
longFlag = flagSplit[0];
String value = flagSplit[1];
CommandElement element = longFlags.get(longFlag.toLowerCase());
if (element == null) { // Whole flag is specified, we'll go to value (even though flag is unknown
if (unknownLongFlagBehavior == UnknownFlagBehavior.IGNORE) {
} else {
context.putArg(longFlag, value);
}
} else {
args.insertArg(value);
final String finalLongFlag = longFlag;
int position = args.getPosition();
try {
element.parse(args, context);
} catch (ArgumentParseException ex) {
args.setPosition(position);
return ImmutableList.copyOf(Iterables.transform(element.tabComplete(src, args, context), input -> "--" + finalLongFlag + "=" + input));
}
}
} else {
CommandElement element = longFlags.get(longFlag.toLowerCase());
if (element == null) {
return ImmutableList.copyOf(Iterables.transform(Iterables.filter(longFlags.keySet(), new GuavaStartsWithPredicate(longFlag.toLowerCase())), input -> "--" + input));
} else {
boolean complete = false;
int position = args.getPosition();
try {
element.parse(args, context);
} catch (ArgumentParseException ex) {
complete = true;
}
if (!args.hasNext()) {
complete = true;
}
if (complete) {
args.setPosition(position);
return element.tabComplete(src, args, context);
}
}
}
return null;
}
private <TextType> List<String> tabCompleteShortFlags(String shortFlags, Commander<TextType> src, CommandArgs args, CommandContext context) {
if (shortFlags.isEmpty()) {
return null;
}
for (int i = 0; i < shortFlags.length(); ++i) {
final String flagChar = shortFlags.substring(i, i + 1);
CommandElement element = this.shortFlags.get(flagChar);
if (element == null) {
continue;
}
int start = args.getPosition();
try {
element.parse(args, context);
} catch (ArgumentParseException ex) {
args.setPosition(start);
return element.tabComplete(src, args, context);
}
}
return null;
}
}
/**
* Consumes a series of arguments. Usage is the elements concated
*
* @param elements The series of arguments to expect
* @return the element to match the input
*/
public static CommandElement seq(CommandElement... elements) {
return new SequenceCommandElement(ImmutableList.copyOf(elements));
}
private static class SequenceCommandElement extends CommandElement {
private final List<CommandElement> elements;
private SequenceCommandElement(List<CommandElement> elements) {
super(null);
this.elements = elements;
}
@Override
public void parse(CommandArgs args, CommandContext context) throws ArgumentParseException {
for (CommandElement element : elements) {
element.parse(args, context);
}
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return null;
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
for (Iterator<CommandElement> it = elements.iterator(); it.hasNext(); ) {
CommandElement element = it.next();
int startPos = args.getPosition();
try {
element.parse(args, context);
int endPos = args.getPosition();
if (!args.hasNext()) {
args.setPosition(startPos);
List<String> inputs = element.tabComplete(src, args, context);
args.setPosition(args.getPosition() - 1);
if (!inputs.contains(args.next())) { // Tabcomplete returns results to complete the last word in an argument.
// If the last word is one of the completions, the command is most likely complete
return inputs;
}
args.setPosition(endPos);
}
} catch (ArgumentParseException e) {
args.setPosition(startPos);
return element.tabComplete(src, args, context);
}
if (!it.hasNext()) {
args.setPosition(startPos);
}
}
return Collections.emptyList();
}
@Override
public <TextType> TextType getUsage(Commander<TextType> commander) {
final List<Object> ret = new ArrayList<>(Math.max(0, elements.size() * 2 - 1));
for (Iterator<CommandElement> it = elements.iterator(); it.hasNext();) {
ret.add(it.next().getUsage(commander));
if (it.hasNext()) {
ret.add(' ');
}
}
return commander.fmt().combined(ret.toArray());
}
}
/**
* Return an argument that allows selecting from a limited set of values.
* If there are 5 or fewer choices available, the choices will be shown in the command usage. Otherwise, the usage
* will only display only the key. To override this behavior, see {@link #choices(Translatable, Map, boolean)}.
*
* @param key The key to store the resulting value under
* @param choices The choices users can choose from
* @return the element to match the input
*/
public static CommandElement choices(Translatable key, Map<String, ?> choices) {
return choices(key, choices, choices.size() <= 5);
}
/**
* Return an argument that allows selecting from a limited set of values.
* Unless {@code choicesInUsage} is true, general command usage will only display the provided key
*
* @param key The key to store the resulting value under
* @param choices The choices users can choose from
* @return the element to match the input
*/
public static CommandElement choices(Translatable key, Map<String, ?> choices, boolean choicesInUsage) {
return new ChoicesCommandElement(key, ImmutableMap.copyOf(choices), choicesInUsage);
}
private static class ChoicesCommandElement extends CommandElement {
private final Map<String, Object> choices;
private final boolean choicesInUsage;
private ChoicesCommandElement(Translatable key, Map<String, Object> choices, boolean choicesInUsage) {
super(key);
this.choices = choices;
this.choicesInUsage = choicesInUsage;
}
@Override
public Object parseValue(CommandArgs args) throws ArgumentParseException {
Object value = choices.get(args.next());
if (value == null) {
throw args.createError(t("Argument was not a valid choice. Valid choices: %s", choices.keySet().toString()));
}
return value;
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
final String prefix = args.nextIfPresent().orElse("");
return ImmutableList.copyOf(Iterables.filter(choices.keySet(), new GuavaStartsWithPredicate(prefix)));
}
@Override
public <TextType> TextType getUsage(Commander<TextType> commander) {
if (choicesInUsage) {
final List<Object> args = new ArrayList<>(Math.max(0, choices.size() * 2 - 1));
for (Iterator<String> it = choices.keySet().iterator(); it.hasNext();) {
args.add(it.next());
if (it.hasNext()) {
args.add("|");
}
}
return commander.fmt().combined(args.toArray());
} else {
return commander.fmt().tr(getKey());
}
}
}
/**
* Returns a command element that matches the first of the provided elements that parses
* Tab completion matches from all options
*
* @param elements The elements to check against
* @return The command element matching the first passing of the elements provided
*/
public static CommandElement firstParsing(CommandElement... elements) {
return new FirstParsingCommandElement(ImmutableList.copyOf(elements));
}
private static class FirstParsingCommandElement extends CommandElement {
private final List<CommandElement> elements;
private FirstParsingCommandElement(List<CommandElement> elements) {
super(null);
this.elements = elements;
}
@Override
public void parse(CommandArgs args, CommandContext context) throws ArgumentParseException {
ArgumentParseException firstException = null;
for (CommandElement element : elements) {
int startIndex = args.getPosition();
try {
element.parse(args, context);
return;
} catch (ArgumentParseException ex) {
if (firstException == null) {
firstException = ex;
}
args.setPosition(startIndex); // TODO: roll back commandcontext too when parsing fails
}
}
if (firstException != null) {
throw firstException;
}
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return null;
}
@Override
public <TextType> List<String> tabComplete(final Commander<TextType> src, final CommandArgs args, final CommandContext context) {
return ImmutableList.copyOf(Iterables.concat(Iterables.transform(elements, input -> {
int startIndex = args.getPosition();
List<String> ret = input.tabComplete(src, args, context);
args.setPosition(startIndex);
return ret;
})));
}
@Override
public <TextType> TextType getUsage(Commander<TextType> commander) {
final List<Object> ret = new ArrayList<>(Math.max(0, elements.size() * 2 - 1));
for (Iterator<CommandElement> it = elements.iterator(); it.hasNext();) {
ret.add(it.next().getUsage(commander));
if (it.hasNext()) {
ret.add('|');
}
}
return commander.fmt().combined(ret.toArray());
}
}
/**
* Make the provided command element optional
* This means the command element is not required. However, if the element is provided with invalid format and there
* are no more args specified, any errors will still be passed on.
*
* @param element The element to optionally require
* @return the element to match the input
*/
public static CommandElement optional(CommandElement element) {
return new OptionalCommandElement(element, null, false);
}
/**
* Make the provided command element optional
* This means the command element is not required.
* If the argument is provided but of invalid format, it will be skipped.
*
* @param element The element to optionally require
* @return the element to match the input
*/
public static CommandElement optionalWeak(CommandElement element) {
return new OptionalCommandElement(element, null, true);
}
/**
* Make the provided command element optional
* This means the command element is not required. However, if the element is provided with invalid format and there
* are no more args specified, any errors will still be passed on. If the given element's key and {@code value} are not
* null and this element is not provided the element's key will be set to the given value.
*
* @param element The element to optionally require
* @param value The default value to set
* @return the element to match the input
*/
public static CommandElement optional(CommandElement element, Object value) {
return new OptionalCommandElement(element, value, false);
}
/**
* Make the provided command element optional
* This means the command element is not required.
* If the argument is provided but of invalid format, it will be skipped.
* If the given element's key and {@code value} are not null and this element is not provided the element's key will
* be set to the given value.
*
* @param element The element to optionally require
* @param value The default value to set
* @return the element to match the input
*/
public static CommandElement optionalWeak(CommandElement element, Object value) {
return new OptionalCommandElement(element, value, true);
}
private static class OptionalCommandElement extends CommandElement {
private final CommandElement element;
private final Object value;
private final boolean considerInvalidFormatEmpty;
private OptionalCommandElement(CommandElement element, Object value, boolean considerInvalidFormatEmpty) {
super(null);
this.element = element;
this.value = value;
this.considerInvalidFormatEmpty = considerInvalidFormatEmpty;
}
@Override
public void parse(CommandArgs args, CommandContext context) throws ArgumentParseException {
if (!args.hasNext()) {
if (this.element.getKey() != null && this.value != null) {
context.putArg(this.element.getKey().getUntranslated(), value);
}
return;
}
int startPos = args.getPosition();
try {
element.parse(args, context);
} catch (ArgumentParseException ex) {
if (considerInvalidFormatEmpty || args.hasNext()) { // If there are more args, suppress. Otherwise, throw the error
args.setPosition(startPos);
if (this.element.getKey() != null && this.value != null) {
context.putArg(this.element.getKey().getUntranslated(), value);
}
} else {
throw ex;
}
}
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return args.hasNext() ? null : element.parseValue(args);
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
return element.tabComplete(src, args, context);
}
@Override
public <TextType> TextType getUsage(Commander<TextType> src) {
return src.fmt().combined("[", this.element.getUsage(src), "]");
}
}
/**
* Require a given command element to be provided a certain number of times
* Command values will be stored under their provided keys in the CommandContext
*
* @param element The element to repeat
* @param times The number of times to repeat the element.
* @return the element to match the input
*/
public static CommandElement repeated(CommandElement element, int times) {
return new RepeatedCommandElement(element, times);
}
private static class RepeatedCommandElement extends CommandElement {
private final CommandElement element;
private final int times;
protected RepeatedCommandElement(CommandElement element, int times) {
super(null);
this.element = element;
this.times = times;
}
@Override
public void parse(CommandArgs args, CommandContext context) throws ArgumentParseException {
for (int i = 0; i < times; ++i) {
element.parse(args, context);
}
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return null;
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
for (int i = 0; i < times; ++i) {
int startPos = args.getPosition();
try {
element.parse(args, context);
} catch (ArgumentParseException e) {
args.setPosition(startPos);
return element.tabComplete(src, args, context);
}
}
return Collections.emptyList();
}
@Override
public <TextType> TextType getUsage(Commander<TextType> src) {
return src.fmt().combined(times, '*', element.getUsage(src));
}
}
/**
* Require all remaining args to match as many instances of CommandElement as will fit
* Command element values will be stored under their provided keys in the CommandContext.
*
* @param element The element to repeat
* @return the element to match the input
*/
public static CommandElement allOf(CommandElement element) {
return new AllOfCommandElement(element);
}
private static class AllOfCommandElement extends CommandElement {
private final CommandElement element;
protected AllOfCommandElement(CommandElement element) {
super(null);
this.element = element;
}
@Override
public void parse(CommandArgs args, CommandContext context) throws ArgumentParseException {
while (args.hasNext()) {
element.parse(args, context);
}
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return null;
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
while (args.hasNext()) {
int startPos = args.getPosition();
try {
element.parse(args, context);
} catch (ArgumentParseException e) {
args.setPosition(startPos);
return element.tabComplete(src, args, context);
}
}
return Collections.emptyList();
}
@Override
public <TextType> TextType getUsage(Commander<TextType> context) {
return context.fmt().combined(element.getUsage(context), '+');
}
}
// -- Argument types for basic java types
/**
* Parent class that specifies elemenents as having no tab completions. Useful for inputs with a very large domain, like strings and integers
*/
private static abstract class KeyElement extends CommandElement {
private KeyElement(Translatable key) {
super(key);
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
return Collections.emptyList();
}
}
/**
* Require an argument to be a string. Any provided argument will fit in under this argument
*
* @param key The key to store the parsed argument under
* @return the element to match the input
*/
public static CommandElement string(Translatable key) {
return new StringElement(key);
}
private static class StringElement extends KeyElement {
private StringElement(Translatable key) {
super(key);
}
@Override
public Object parseValue(CommandArgs args) throws ArgumentParseException {
return args.next();
}
}
/**
* Require an argument to be an integer (base 10).
*
* @param key The key to store the parsed argument under
* @return the element to match the input
*/
public static CommandElement integer(Translatable key) {
return new IntegerElement(key);
}
private static class IntegerElement extends KeyElement {
private IntegerElement(Translatable key) {
super(key);
}
@Override
public Object parseValue(CommandArgs args) throws ArgumentParseException {
final String input = args.next();
try {
return Integer.parseInt(input);
} catch (NumberFormatException ex) {
throw args.createError(t("Expected an integer, but input '%s' was not", input));
}
}
}
private static final Map<String, Boolean> BOOLEAN_CHOICES = ImmutableMap.<String, Boolean>builder()
.put("true", true)
.put("t", true)
.put("y", true)
.put("yes", true)
.put("verymuchso", true)
.put("false", false)
.put("f", false)
.put("n", false)
.put("no", false)
.put("notatall", false)
.build();
/**
* Require an argument to be a boolean.
* The recognized true values are:
* <ul>
* <li>true</li>
* <li>t</li>
* <li>yes</li>
* <li>y</li>
* <li>verymuchso</li>
* </ul>
* The recognized false values are:
* <ul>
* <li>false</li>
* <li>f</li>
* <li>no</li>
* <li>n</li>
* <li>notatall</li>
* </ul>
*
* @param key The key to store the parsed argument under
* @return the element to match the input
*/
public static CommandElement bool(Translatable key) {
return GenericArguments.choices(key, BOOLEAN_CHOICES);
}
/**
* Require the argument to be a key under the provided enum
* @param key The key to store the matched enum value under
* @param type The enum class to get enum constants from
* @param <T> The type of enum
* @return the element to match the input
*/
public static <T extends Enum<T>> CommandElement enumValue(Translatable key, Class<T> type) {
return new EnumValueElement<>(key, type);
}
private static class EnumValueElement<T extends Enum<T>> extends CommandElement {
private final Class<T> type;
private EnumValueElement(Translatable key, Class<T> type) {
super(key);
this.type = type;
}
@Override
public Object parseValue(CommandArgs args) throws ArgumentParseException {
final String value = args.next().toUpperCase();
try {
return Enum.valueOf(type, value);
} catch (IllegalArgumentException ex) {
throw args.createError(t("Enum value %s not valid", value));
}
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
Iterable<String> validValues = Iterables.transform(Arrays.asList(type.getEnumConstants()), Enum::name);
if (args.hasNext()) {
try {
final String prefix = args.next();
validValues = Iterables.filter(validValues, new GuavaStartsWithPredicate(prefix));
} catch (ArgumentParseException ignore) {
}
}
return ImmutableList.copyOf(validValues);
}
}
/**
* Require one or more strings, which are combined into a single, space-separated string.
*
* @param key The key to store the parsed argument under
* @return the element to match the input
*/
public static CommandElement remainingJoinedStrings(Translatable key) {
return new RemainingJoinedStringsCommandElement(key, false);
}
private static class RemainingJoinedStringsCommandElement extends KeyElement {
private final boolean raw;
private RemainingJoinedStringsCommandElement(Translatable key, boolean raw) {
super(key);
this.raw = raw;
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
if (raw) {
args.next();
ArgumentParseException ex = args.createError(null);
String ret = args.getRaw().substring(ex.getPosition());
while (args.hasNext()) {
args.next();
}
return ret;
} else {
final StringBuilder ret = new StringBuilder(args.next());
while (args.hasNext()) {
ret.append(' ').append(args.next());
}
return ret.toString();
}
}
@Override
public <TextType> TextType getUsage(Commander<TextType> src) {
return src.fmt().combined(super.getUsage(src), "...");
}
}
/**
* Expect a literal sequence of arguments. This element matches the input against a predefined array of arguments expected to be present,
* case-insensitively.
*
* @param key The key to add to the context. Will be set to a value of true if this element matches
* @param expectedArgs The sequence of arguments expected
* @return the appropriate command element
*/
public static CommandElement literal(Translatable key, String... expectedArgs) {
return new LiteralCommandElement(key, ImmutableList.copyOf(expectedArgs), true);
}
/**
* Expect a literal sequence of arguments. This element matches the input against a predefined array of arguments expected to be present,
* case-insensitively.
*
* @param key The key to store this argument as
* @param putValue The value to put at key if this argument matches. May be null
* @param expectedArgs The sequence of arguments expected
* @return the appropriate command element
*/
public static CommandElement literal(Translatable key, Object putValue, String... expectedArgs) {
return new LiteralCommandElement(key, ImmutableList.copyOf(expectedArgs), putValue);
}
private static class LiteralCommandElement extends CommandElement {
private final List<String> expectedArgs;
private final Object putValue;
protected LiteralCommandElement(@Nullable Translatable key, List<String> expectedArgs, Object putValue) {
super(key);
this.expectedArgs = ImmutableList.copyOf(expectedArgs);
this.putValue = putValue;
}
@Nullable
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
for (String arg : this.expectedArgs) {
String current;
if (!(current = args.next()).equalsIgnoreCase(arg)) {
throw args.createError(t("Argument %s did not match expected next argument %s", current, arg));
}
}
return this.putValue;
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext ctx) {
for (String arg : this.expectedArgs) {
final Optional<String> next = args.nextIfPresent();
if (!next.isPresent()) {
break;
} else if (args.hasNext()) {
if (!next.get().equalsIgnoreCase(arg)) {
break;
}
} else {
if (arg.toLowerCase().startsWith(next.get().toLowerCase())) { // Case-insensitive compare
return ImmutableList.of(arg); // TODO: Possibly complete all remaining args? Does that even work
}
}
}
return ImmutableList.of();
}
@Override
public <TextType> TextType getUsage(Commander<TextType> src) {
return src.fmt().combined(Joiner.on(' ').join(this.expectedArgs));
}
}
public static CommandElement suggestibleString(Translatable key, Supplier<Collection<String>> keySupplier) {
return new SuggestibleStringCommandElement(key, keySupplier);
}
private static class SuggestibleStringCommandElement extends CommandElement {
private final Supplier<Collection<String>> keySupplier;
public SuggestibleStringCommandElement(Translatable key, Supplier<Collection<String>> keySupplier) {
super(key);
this.keySupplier = keySupplier;
}
@Override
protected Object parseValue(CommandArgs args) throws ArgumentParseException {
return args.next();
}
@Override
public <TextType> List<String> tabComplete(Commander<TextType> src, CommandArgs args, CommandContext context) {
return args.nextIfPresent()
.map(arg -> keySupplier.get().stream().filter(new StartsWithPredicate(arg))
.collect(GuavaCollectors.toImmutableList()))
.orElse(ImmutableList.of());
}
}
}