From 04c31262f720e83c293e4bf3eaa7709b3dbd7813 Mon Sep 17 00:00:00 2001 From: Albert Pham Date: Tue, 18 Jun 2013 14:50:46 -0700 Subject: [PATCH] Added a new command dispatcher that injects different parameters dynamically. This reduces the boilerplate code needed to parse arguments in each command, and reduces the need to maintain command documentation with @Command. Example: @Command(aliases = "/set", desc = "Set all the blocks inside the selection to a block") @CommandPermissions("worldedit.region.set") @Logging(REGION) void setBlocks(LocalPlayer player, EditSession editSession, @Selection Region region, Pattern replaceWith) { // Perform command } --- pom.xml | 65 +++ .../util/commands/CommandContext.java | 78 ++- .../util/commands/CommandException.java | 35 ++ .../util/commands/CommandLocals.java | 49 ++ .../util/commands/SuggestionContext.java | 49 ++ .../sk89q/rebar/command/CommandCallable.java | 63 ++ .../sk89q/rebar/command/CommandMapping.java | 78 +++ .../com/sk89q/rebar/command/Description.java | 69 +++ .../com/sk89q/rebar/command/Dispatcher.java | 112 ++++ .../rebar/command/InvalidUsageException.java | 48 ++ .../command/MissingParameterException.java | 30 + .../com/sk89q/rebar/command/Parameter.java | 65 +++ .../rebar/command/SimpleDescription.java | 128 +++++ .../sk89q/rebar/command/SimpleDispatcher.java | 127 +++++ .../command/SimpleDispatcherCommand.java | 99 ++++ .../sk89q/rebar/command/SimpleParameter.java | 130 +++++ .../command/UnconsumedParameterException.java | 41 ++ .../command/binding/PrimitiveBindings.java | 258 +++++++++ .../sk89q/rebar/command/binding/Range.java | 49 ++ .../command/binding/StandardBindings.java | 45 ++ .../sk89q/rebar/command/binding/Switch.java | 43 ++ .../com/sk89q/rebar/command/binding/Text.java | 40 ++ .../sk89q/rebar/command/binding/Validate.java | 44 ++ .../rebar/command/fluent/CommandGraph.java | 83 +++ .../rebar/command/fluent/DispatcherNode.java | 141 +++++ .../parametric/AbstractInvokeListener.java | 35 ++ .../command/parametric/ArgumentStack.java | 77 +++ .../rebar/command/parametric/Binding.java | 91 +++ .../command/parametric/BindingBehavior.java | 51 ++ .../command/parametric/BindingHelper.java | 223 ++++++++ .../command/parametric/BindingMatch.java | 70 +++ .../parametric/ContextArgumentStack.java | 177 ++++++ .../parametric/ExceptionConverter.java | 51 ++ .../parametric/ExceptionConverterHelper.java | 108 ++++ .../command/parametric/ExceptionMatch.java | 33 ++ .../command/parametric/InvokeHandler.java | 81 +++ .../command/parametric/InvokeListener.java | 57 ++ .../parametric/LegacyCommandsHandler.java | 96 ++++ .../rebar/command/parametric/Optional.java | 40 ++ .../command/parametric/ParameterData.java | 193 +++++++ .../parametric/ParameterException.java | 44 ++ .../command/parametric/ParametricBuilder.java | 204 +++++++ .../parametric/ParametricCallable.java | 537 ++++++++++++++++++ .../parametric/ParametricException.java | 27 + .../parametric/PermissionsHandler.java | 66 +++ .../parametric/StringArgumentStack.java | 127 +++++ 46 files changed, 4350 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/sk89q/minecraft/util/commands/CommandLocals.java create mode 100644 src/main/java/com/sk89q/minecraft/util/commands/SuggestionContext.java create mode 100644 src/main/java/com/sk89q/rebar/command/CommandCallable.java create mode 100644 src/main/java/com/sk89q/rebar/command/CommandMapping.java create mode 100644 src/main/java/com/sk89q/rebar/command/Description.java create mode 100644 src/main/java/com/sk89q/rebar/command/Dispatcher.java create mode 100644 src/main/java/com/sk89q/rebar/command/InvalidUsageException.java create mode 100644 src/main/java/com/sk89q/rebar/command/MissingParameterException.java create mode 100644 src/main/java/com/sk89q/rebar/command/Parameter.java create mode 100644 src/main/java/com/sk89q/rebar/command/SimpleDescription.java create mode 100644 src/main/java/com/sk89q/rebar/command/SimpleDispatcher.java create mode 100644 src/main/java/com/sk89q/rebar/command/SimpleDispatcherCommand.java create mode 100644 src/main/java/com/sk89q/rebar/command/SimpleParameter.java create mode 100644 src/main/java/com/sk89q/rebar/command/UnconsumedParameterException.java create mode 100644 src/main/java/com/sk89q/rebar/command/binding/PrimitiveBindings.java create mode 100644 src/main/java/com/sk89q/rebar/command/binding/Range.java create mode 100644 src/main/java/com/sk89q/rebar/command/binding/StandardBindings.java create mode 100644 src/main/java/com/sk89q/rebar/command/binding/Switch.java create mode 100644 src/main/java/com/sk89q/rebar/command/binding/Text.java create mode 100644 src/main/java/com/sk89q/rebar/command/binding/Validate.java create mode 100644 src/main/java/com/sk89q/rebar/command/fluent/CommandGraph.java create mode 100644 src/main/java/com/sk89q/rebar/command/fluent/DispatcherNode.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/AbstractInvokeListener.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ArgumentStack.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/Binding.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/BindingBehavior.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/BindingHelper.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/BindingMatch.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ContextArgumentStack.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverter.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverterHelper.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ExceptionMatch.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/InvokeHandler.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/InvokeListener.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/LegacyCommandsHandler.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/Optional.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ParameterData.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ParameterException.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ParametricBuilder.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ParametricCallable.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/ParametricException.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/PermissionsHandler.java create mode 100644 src/main/java/com/sk89q/rebar/command/parametric/StringArgumentStack.java diff --git a/pom.xml b/pom.xml index d01870078..55acf6510 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,13 @@ http://repo.bukkit.org/content/groups/public + + + + sk89q-repo + http://maven.sk89q.com/repo/ + + @@ -112,6 +119,12 @@ jar true + + + com.thoughtworks.paranamer + paranamer + 2.5.2 + org.bukkit @@ -191,6 +204,25 @@ 1.6 + + + com.thoughtworks.paranamer + paranamer-maven-plugin-largestack + 2.5.5-SNAPSHOT + + + run + compile + + ${project.build.sourceDirectory} + ${project.build.outputDirectory} + + + generate + + + + org.apache.maven.plugins @@ -244,6 +276,7 @@ com.sk89q:jchronic + com.thoughtworks.paranamer:paranamer @@ -302,8 +335,40 @@ + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + com.thoughtworks.paranamer + paranamer-maven-plugin-largestack + [2.5.5-SNAPSHOT,) + + generate + + + + + + + + + + + + + diff --git a/src/main/java/com/sk89q/minecraft/util/commands/CommandContext.java b/src/main/java/com/sk89q/minecraft/util/commands/CommandContext.java index 0a36b7e98..6b583682a 100644 --- a/src/main/java/com/sk89q/minecraft/util/commands/CommandContext.java +++ b/src/main/java/com/sk89q/minecraft/util/commands/CommandContext.java @@ -27,15 +27,22 @@ import java.util.Map; import java.util.Set; public class CommandContext { + protected final String command; protected final List parsedArgs; protected final List originalArgIndices; protected final String[] originalArgs; protected final Set booleanFlags = new HashSet(); protected final Map valueFlags = new HashMap(); + protected final SuggestionContext suggestionContext; + protected final CommandLocals locals; + + public static String[] split(String args) { + return args.split(" ", -1); + } public CommandContext(String args) throws CommandException { - this(args.split(" "), null); + this(args.split(" ", -1), null); } public CommandContext(String[] args) throws CommandException { @@ -43,28 +50,50 @@ public class CommandContext { } public CommandContext(String args, Set valueFlags) throws CommandException { - this(args.split(" "), valueFlags); + this(args.split(" ", -1), valueFlags); + } + + public CommandContext(String args, Set valueFlags, boolean allowHangingFlag) + throws CommandException { + this(args.split(" ", -1), valueFlags, allowHangingFlag, new CommandLocals()); + } + + public CommandContext(String[] args, Set valueFlags) throws CommandException { + this(args, valueFlags, false, null); } /** - * @param args An array with arguments. Empty strings outside quotes will be removed. - * @param valueFlags A set containing all value flags. Pass null to disable value flag parsing. - * @throws CommandException This is thrown if flag fails for some reason. + * Parse the given array of arguments. + * + *

Empty arguments are removed from the list of arguments.

+ * + * @param args an array with arguments + * @param valueFlags a set containing all value flags (pass null to disable value flag parsing) + * @param allowHangingFlag true if hanging flags are allowed + * @param locals the locals, null to create empty one + * @throws CommandException thrown on a parsing error */ - public CommandContext(String[] args, Set valueFlags) throws CommandException { + public CommandContext(String[] args, Set valueFlags, + boolean allowHangingFlag, CommandLocals locals) throws CommandException { if (valueFlags == null) { valueFlags = Collections.emptySet(); } originalArgs = args; command = args[0]; + this.locals = locals != null ? locals : new CommandLocals(); + boolean isHanging = false; + SuggestionContext suggestionContext = SuggestionContext.hangingValue(); // Eliminate empty args and combine multiword args first List argIndexList = new ArrayList(args.length); List argList = new ArrayList(args.length); for (int i = 1; i < args.length; ++i) { + isHanging = false; + String arg = args[i]; if (arg.length() == 0) { + isHanging = true; continue; } @@ -112,9 +141,14 @@ public class CommandContext { for (int nextArg = 0; nextArg < argList.size(); ) { // Fetch argument String arg = argList.get(nextArg++); + suggestionContext = SuggestionContext.hangingValue(); // Not a flag? if (arg.charAt(0) != '-' || arg.length() == 1 || !arg.matches("^-[a-zA-Z]+$")) { + if (!isHanging) { + suggestionContext = SuggestionContext.lastValue(); + } + originalArgIndices.add(argIndexList.get(nextArg - 1)); parsedArgs.add(arg); continue; @@ -139,16 +173,30 @@ public class CommandContext { } if (nextArg >= argList.size()) { - throw new CommandException("No value specified for the '-" + flagName + "' flag."); + if (allowHangingFlag) { + suggestionContext = SuggestionContext.flag(flagName); + break; + } else { + throw new CommandException("No value specified for the '-" + flagName + "' flag."); + } } // If it is a value flag, read another argument and add it this.valueFlags.put(flagName, argList.get(nextArg++)); + if (!isHanging) { + suggestionContext = SuggestionContext.flag(flagName); + } } else { booleanFlags.add(flagName); } } } + + this.suggestionContext = suggestionContext; + } + + public SuggestionContext getSuggestionContext() { + return suggestionContext; } public String getCommand() { @@ -175,6 +223,18 @@ public class CommandContext { } return buffer.toString(); } + + public String getRemainingString(int start) { + return getString(start, parsedArgs.size() - 1); + } + + public String getString(int start, int end) { + StringBuilder buffer = new StringBuilder(parsedArgs.get(start)); + for (int i = start + 1; i < end + 1; ++i) { + buffer.append(" ").append(parsedArgs.get(i)); + } + return buffer.toString(); + } public int getInteger(int index) throws NumberFormatException { return Integer.parseInt(parsedArgs.get(index)); @@ -258,4 +318,8 @@ public class CommandContext { public int argsLength() { return parsedArgs.size(); } + + public CommandLocals getLocals() { + return locals; + } } diff --git a/src/main/java/com/sk89q/minecraft/util/commands/CommandException.java b/src/main/java/com/sk89q/minecraft/util/commands/CommandException.java index 6a3e7e1a4..577b9a7ff 100644 --- a/src/main/java/com/sk89q/minecraft/util/commands/CommandException.java +++ b/src/main/java/com/sk89q/minecraft/util/commands/CommandException.java @@ -19,8 +19,14 @@ package com.sk89q.minecraft.util.commands; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + public class CommandException extends Exception { + private static final long serialVersionUID = 870638193072101739L; + private List commandStack = new ArrayList(); public CommandException() { super(); @@ -30,8 +36,37 @@ public class CommandException extends Exception { super(message); } + public CommandException(String message, Throwable t) { + super(message, t); + } + public CommandException(Throwable t) { super(t); } + public void prependStack(String name) { + commandStack.add(name); + } + + public String toStackString(String prefix, String spacedSuffix) { + StringBuilder builder = new StringBuilder(); + if (prefix != null) { + builder.append(prefix); + } + ListIterator li = commandStack.listIterator(commandStack.size()); + while (li.hasPrevious()) { + if (li.previousIndex() != commandStack.size() - 1) { + builder.append(" "); + } + builder.append(li.previous()); + } + if (spacedSuffix != null) { + if (builder.length() > 0) { + builder.append(" "); + } + builder.append(spacedSuffix); + } + return builder.toString(); + } + } diff --git a/src/main/java/com/sk89q/minecraft/util/commands/CommandLocals.java b/src/main/java/com/sk89q/minecraft/util/commands/CommandLocals.java new file mode 100644 index 000000000..368036c5f --- /dev/null +++ b/src/main/java/com/sk89q/minecraft/util/commands/CommandLocals.java @@ -0,0 +1,49 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.minecraft.util.commands; + +import java.util.HashMap; +import java.util.Map; + +public class CommandLocals { + + private final Map locals = new HashMap(); + + public boolean containsKey(Object key) { + return locals.containsKey(key); + } + + public boolean containsValue(Object value) { + return locals.containsValue(value); + } + + public Object get(Object key) { + return locals.get(key); + } + + @SuppressWarnings("unchecked") + public T get(Class key) { + return (T) locals.get(key); + } + + public Object put(Object key, Object value) { + return locals.put(key, value); + } + +} diff --git a/src/main/java/com/sk89q/minecraft/util/commands/SuggestionContext.java b/src/main/java/com/sk89q/minecraft/util/commands/SuggestionContext.java new file mode 100644 index 000000000..425588417 --- /dev/null +++ b/src/main/java/com/sk89q/minecraft/util/commands/SuggestionContext.java @@ -0,0 +1,49 @@ +package com.sk89q.minecraft.util.commands; + +public class SuggestionContext { + + private static final SuggestionContext FOR_LAST = new SuggestionContext(null, true); + private static final SuggestionContext FOR_HANGING = new SuggestionContext(null, false); + + private final Character flag; + private final boolean forLast; + + private SuggestionContext(Character flag, boolean forLast) { + this.flag = flag; + this.forLast = forLast; + } + + public boolean forHangingValue() { + return flag == null && !forLast; + } + + public boolean forLastValue() { + return flag == null && forLast; + } + + public boolean forFlag() { + return flag != null; + } + + public Character getFlag() { + return flag; + } + + @Override + public String toString() { + return forFlag() ? ("-" + getFlag()) : (forHangingValue() ? "hanging" : "last"); + } + + public static SuggestionContext flag(Character flag) { + return new SuggestionContext(flag, false); + } + + public static SuggestionContext lastValue() { + return FOR_LAST; + } + + public static SuggestionContext hangingValue() { + return FOR_HANGING; + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/CommandCallable.java b/src/main/java/com/sk89q/rebar/command/CommandCallable.java new file mode 100644 index 000000000..2c3bd6750 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/CommandCallable.java @@ -0,0 +1,63 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import java.util.Collection; +import java.util.Set; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; + +/** + * A command that can be executed. + */ +public interface CommandCallable { + + /** + * Get a list of value flags used by this command. + * + * @return a list of value flags + */ + Set getValueFlags(); + + /** + * Execute the command. + * + * @param context the user input + * @throws CommandException thrown on any sort of command exception + */ + void call(CommandContext context) throws CommandException; + + /** + * Get a list of suggestions. + * + * @param context the user input + * @return a list of suggestions + * @throws CommandException + */ + Collection getSuggestions(CommandContext context) throws CommandException; + + /** + * Get an object describing this command. + * + * @return the command description + */ + Description getDescription(); + +} diff --git a/src/main/java/com/sk89q/rebar/command/CommandMapping.java b/src/main/java/com/sk89q/rebar/command/CommandMapping.java new file mode 100644 index 000000000..53f03811b --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/CommandMapping.java @@ -0,0 +1,78 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + + +/** + * Tracks a command registration. + */ +public class CommandMapping { + + private final String[] aliases; + private final CommandCallable callable; + + /** + * Create a new instance. + * + * @param callable the command callable + * @param alias a list of all aliases, where the first one is the primary one + */ + public CommandMapping(CommandCallable callable, String... alias) { + super(); + this.aliases = alias; + this.callable = callable; + } + + /** + * Get the primary alias. + * + * @return the primary alias + */ + public String getPrimaryAlias() { + return aliases[0]; + } + + /** + * Get a list of all aliases. + * + * @return aliases + */ + public String[] getAllAliases() { + return aliases; + } + + /** + * Get the callable + * + * @return the callable + */ + public CommandCallable getCallable() { + return callable; + } + + /** + * Get the {@link Description} form the callable. + * + * @return the description + */ + public Description getDescription() { + return getCallable().getDescription(); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/Description.java b/src/main/java/com/sk89q/rebar/command/Description.java new file mode 100644 index 000000000..ff08ea43b --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/Description.java @@ -0,0 +1,69 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import java.util.List; + +/** + * A description of a command. + */ +public interface Description { + + /** + * Get the list of parameters for this command. + * + * @return a list of parameters + */ + List getParameters(); + + /** + * Get a short one-line description of this command. + * + * @return a description, or null if no description is available + */ + String getDescription(); + + /** + * Get a longer help text about this command. + * + * @return a help text, or null if no help is available + */ + String getHelp(); + + /** + * Get the usage string of this command. + * + *

A usage string may look like + * [-w <world>] <var1> <var2>.

+ * + * @return a usage string + */ + String getUsage(); + + /** + * Get a list of permissions that the player may have to have permission. + * + *

Permission data may or may not be available. This is only useful as a + * potential hint.

+ * + * @return the list of permissions + */ + List getPermissions(); + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/rebar/command/Dispatcher.java b/src/main/java/com/sk89q/rebar/command/Dispatcher.java new file mode 100644 index 000000000..5427dede1 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/Dispatcher.java @@ -0,0 +1,112 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import java.util.Collection; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.CommandLocals; + +/** + * Executes a command based on user input. + */ +public interface Dispatcher { + + /** + * Register a command with this dispatcher. + * + * @param callable the command executor + * @param alias a list of aliases, where the first alias is the primary name + */ + void register(CommandCallable callable, String... alias); + + /** + * Get a list of command registrations. + * + *

The returned collection cannot be modified.

+ * + * @return a list of registrations + */ + Collection getCommands(); + + /** + * Get a list of primary aliases. + * + *

The returned collection cannot be modified.

+ * + * @return a list of aliases + */ + Collection getPrimaryAliases(); + + /** + * Get a list of all the command aliases. + * + *

A command may have more than one alias assigned to it. The returned + * collection cannot be modified.

+ * + * @return a list of aliases + */ + Collection getAllAliases(); + + /** + * Get the {@link CommandCallable} associated with an alias. + * + * @param alias the alias + * @return the command mapping + */ + CommandMapping get(String alias); + + /** + * Returns whether the dispatcher contains a registered command for the given alias. + * + * @param alias the alias + * @return true if a registered command exists + */ + boolean contains(String alias); + + /** + * Execute the correct command based on the input. + * + * @param arguments the arguments + * @param locals the locals + * @return the called command, or null if there was no command found + * @throws CommandException thrown on a command error + */ + CommandMapping call(String arguments, CommandLocals locals) throws CommandException; + + /** + * Execute the correct command based on the input. + * + * @param arguments the arguments + * @param locals the locals + * @return the called command, or null if there was no command found + * @throws CommandException thrown on a command error + */ + CommandMapping call(String[] arguments, CommandLocals locals) throws CommandException; + + /** + * Get a list of suggestions based on input. + * + * @param arguments the arguments entered up to this point + * @return a list of suggestions + * @throws CommandException thrown if there was a parsing error + */ + Collection getSuggestions(String arguments) throws CommandException; + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/rebar/command/InvalidUsageException.java b/src/main/java/com/sk89q/rebar/command/InvalidUsageException.java new file mode 100644 index 000000000..f07404e19 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/InvalidUsageException.java @@ -0,0 +1,48 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import com.sk89q.minecraft.util.commands.CommandException; + +/** + * Thrown when a command is not used properly. + */ +public class InvalidUsageException extends CommandException { + + private static final long serialVersionUID = -3222004168669490390L; + private final Description description; + + public InvalidUsageException(Description description) { + this.description = description; + } + + public InvalidUsageException(String message, Description description) { + super(message); + this.description = description; + } + + public Description getDescription() { + return description; + } + + public String getUsage(String prefix) { + return toStackString(prefix, getDescription().getUsage()); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/MissingParameterException.java b/src/main/java/com/sk89q/rebar/command/MissingParameterException.java new file mode 100644 index 000000000..cf7d8581f --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/MissingParameterException.java @@ -0,0 +1,30 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import com.sk89q.rebar.command.parametric.ParameterException; + +/** + * Thrown when there is a missing parameter. + */ +public class MissingParameterException extends ParameterException { + + private static final long serialVersionUID = 2169299987926950535L; + +} diff --git a/src/main/java/com/sk89q/rebar/command/Parameter.java b/src/main/java/com/sk89q/rebar/command/Parameter.java new file mode 100644 index 000000000..16ee33560 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/Parameter.java @@ -0,0 +1,65 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +/** + * Describes a parameter. + * + * @see Description + */ +public interface Parameter { + + /** + * The name of the parameter. + * + * @return the name + */ + String getName(); + + /** + * Get the flag associated with this parameter. + * + * @return the flag, or null if there is no flag associated + * @see #isValueFlag() + */ + Character getFlag(); + + /** + * Return whether the flag is a value flag. + * + * @return true if the flag is a value flag + * @see #getFlag() + */ + boolean isValueFlag(); + + /** + * Get whether this parameter is optional. + * + * @return true if the parameter does not have to be specified + */ + boolean isOptional(); + + /** + * Get the default value as a string to be parsed by the binding. + * + * @return a default value, or null if none is set + */ + public String[] getDefaultValue(); + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/rebar/command/SimpleDescription.java b/src/main/java/com/sk89q/rebar/command/SimpleDescription.java new file mode 100644 index 000000000..1367ff915 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/SimpleDescription.java @@ -0,0 +1,128 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import java.util.Collections; +import java.util.List; + +/** + * A simple implementation of {@link Description} which has setters. + */ +public class SimpleDescription implements Description { + + private List parameters = Collections.emptyList(); + private List permissions = Collections.emptyList(); + private String description; + private String help; + private String overrideUsage; + + @Override + public List getParameters() { + return parameters; + } + + /** + * Set the list of parameters. + * + * @param parameters the list of parameters + * @see #getParameters() + */ + public void setParameters(List parameters) { + this.parameters = Collections.unmodifiableList(parameters); + } + + @Override + public String getDescription() { + return description; + } + + /** + * Set the description of the command. + * + * @param description the description + * @see #getDescription() + */ + public void setDescription(String description) { + this.description = description; + } + + @Override + public String getHelp() { + return help; + } + + /** + * Set the help text of the command. + * + * @param help the help text + * @see #getHelp() + */ + public void setHelp(String help) { + this.help = help; + } + + @Override + public List getPermissions() { + return permissions; + } + + /** + * Set the permissions of this command. + * + * @param permissions the permissions + */ + public void setPermissions(List permissions) { + this.permissions = Collections.unmodifiableList(permissions); + } + + /** + * Override the usage string returned with a given one. + * + * @param usage usage string, or null + */ + public void overrideUsage(String usage) { + this.overrideUsage = usage; + } + + @Override + public String getUsage() { + if (overrideUsage != null) { + return overrideUsage; + } + + StringBuilder builder = new StringBuilder(); + boolean first = true; + + for (Parameter parameter : parameters) { + if (!first) { + builder.append(" "); + } + builder.append(parameter.toString()); + first = false; + } + + return builder.toString(); + } + + @Override + public String toString() { + return getUsage(); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/SimpleDispatcher.java b/src/main/java/com/sk89q/rebar/command/SimpleDispatcher.java new file mode 100644 index 000000000..90b9200bf --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/SimpleDispatcher.java @@ -0,0 +1,127 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.CommandLocals; +import com.sk89q.minecraft.util.commands.WrappedCommandException; + +/** + * A simple implementation of {@link Dispatcher}. + */ +public class SimpleDispatcher implements Dispatcher { + + private final Map commands = new HashMap(); + + @Override + public void register(CommandCallable callable, String... alias) { + CommandMapping mapping = new CommandMapping(callable, alias); + + // Check for replacements + for (String a : alias) { + String lower = a.toLowerCase(); + if (commands.containsKey(lower)) { + throw new IllegalArgumentException( + "Replacing commands is currently undefined behavior"); + } + } + + for (String a : alias) { + String lower = a.toLowerCase(); + commands.put(lower, mapping); + } + } + + @Override + public Collection getCommands() { + return Collections.unmodifiableCollection(commands.values()); + } + + @Override + public Set getAllAliases() { + return Collections.unmodifiableSet(commands.keySet()); + } + + @Override + public Set getPrimaryAliases() { + Set aliases = new HashSet(); + for (CommandMapping mapping : getCommands()) { + aliases.add(mapping.getPrimaryAlias()); + } + return Collections.unmodifiableSet(aliases); + } + + @Override + public boolean contains(String alias) { + return commands.containsKey(alias.toLowerCase()); + } + + @Override + public CommandMapping get(String alias) { + return commands.get(alias.toLowerCase()); + } + + @Override + public CommandMapping call(String arguments, CommandLocals locals) throws CommandException { + return call(CommandContext.split(arguments), locals); + } + + @Override + public CommandMapping call(String[] arguments, CommandLocals locals) throws CommandException { + CommandContext dummyContext = new CommandContext(arguments); + CommandMapping mapping = get(dummyContext.getCommand()); + if (mapping != null) { + CommandCallable c = mapping.getCallable(); + CommandContext context = + new CommandContext(arguments, c.getValueFlags(), false, locals); + try { + c.call(context); + } catch (CommandException e) { + e.prependStack(context.getCommand()); + throw e; + } catch (Throwable t) { + throw new WrappedCommandException(t); + } + } + return mapping; + } + + @Override + public Collection getSuggestions(String arguments) throws CommandException { + CommandContext dummyContext = new CommandContext(arguments); + CommandMapping mapping = get(dummyContext.getCommand()); + if (mapping != null) { + CommandCallable c = mapping.getCallable(); + CommandContext context = + new CommandContext(arguments, c.getValueFlags(), true); + return c.getSuggestions(context); + } + return new ArrayList(); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/SimpleDispatcherCommand.java b/src/main/java/com/sk89q/rebar/command/SimpleDispatcherCommand.java new file mode 100644 index 000000000..99fff7727 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/SimpleDispatcherCommand.java @@ -0,0 +1,99 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; + +/** + * A combination {@link Dispatcher} and {@link CommandCallable} that is backed by + * a {@link SimpleDispatcher}. + * + *

The primary use of this is to make nested commands.

+ */ +public class SimpleDispatcherCommand extends SimpleDispatcher implements CommandCallable { + + private final SimpleDescription description = new SimpleDescription(); + + @Override + public Set getValueFlags() { + return Collections.emptySet(); + } + + @Override + public void call(CommandContext context) throws CommandException { + if (context.argsLength() >= 1) { + super.call(context.getRemainingString(0), context.getLocals()); + } else { + Set aliases = getPrimaryAliases(); + + if (aliases.size() == 0) { + throw new InvalidUsageException( + "This command is supposed to have sub-commands, " + + "but it has no sub-commands.", + getDescription()); + } + + StringBuilder builder = new StringBuilder(); + for (String alias : getPrimaryAliases()) { + builder.append("\n- ").append(alias); + } + + if (aliases.size() == 1) { + builder.append(" (there is only one)"); + } + + throw new InvalidUsageException( + "Select one of these subcommand(s):" + builder.toString(), + getDescription()); + } + } + + @Override + public SimpleDescription getDescription() { + return description; + } + + @Override + public Collection getSuggestions(CommandContext context) throws CommandException { + if (context.argsLength() == 0) { + return super.getAllAliases(); + } else if (context.argsLength() == 1 && + context.getSuggestionContext().forLastValue()) { + String prefix = context.getString(0).toLowerCase(); + List suggestions = new ArrayList(); + for (String alias : super.getAllAliases()) { + if (alias.startsWith(prefix)) { + suggestions.add(alias); + } + } + return suggestions; + } + + return super.getSuggestions( + context.argsLength() > 1 ? context.getRemainingString(1) : ""); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/SimpleParameter.java b/src/main/java/com/sk89q/rebar/command/SimpleParameter.java new file mode 100644 index 000000000..91d9f0ce5 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/SimpleParameter.java @@ -0,0 +1,130 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +/** + * A simple implementation of {@link Parameter} that has setters. + */ +public class SimpleParameter implements Parameter { + + private String name; + private Character flag; + private boolean isValue; + private boolean isOptional; + private String[] defaultValue; + + /** + * Create a new parameter with no name defined yet. + */ + public SimpleParameter() { + } + + /** + * Create a new parameter of the given name. + * + * @param name the name + */ + public SimpleParameter(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + /** + * Set the name of the parameter. + * + * @param name the parameter name + */ + public void setName(String name) { + this.name = name; + } + + @Override + public Character getFlag() { + return flag; + } + + @Override + public boolean isValueFlag() { + return flag != null && isValue; + } + + /** + * Set the flag used by this parameter. + * + * @param flag the flag, or null if there is no flag + * @param isValue true if the flag is a value flag + */ + public void setFlag(Character flag, boolean isValue) { + this.flag = flag; + this.isValue = isValue; + } + + @Override + public boolean isOptional() { + return isOptional || getFlag() != null; + } + + /** + * Set whether this parameter is optional. + * + * @param isOptional true if this parameter is optional + */ + public void setOptional(boolean isOptional) { + this.isOptional = isOptional; + } + + @Override + public String[] getDefaultValue() { + return defaultValue; + } + + /** + * Set the default value. + * + * @param defaultValue a default value, or null if none + */ + public void setDefaultValue(String[] defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (getFlag() != null) { + if (isValueFlag()) { + builder.append("[-") + .append(getFlag()).append(" <").append(getName()).append(">]"); + } else { + builder.append("[-").append(getFlag()).append("]"); + } + } else { + if (isOptional()) { + builder.append("[<").append(getName()).append(">]"); + } else { + builder.append("<").append(getName()).append(">"); + } + } + return builder.toString(); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/UnconsumedParameterException.java b/src/main/java/com/sk89q/rebar/command/UnconsumedParameterException.java new file mode 100644 index 000000000..3e7673a63 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/UnconsumedParameterException.java @@ -0,0 +1,41 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command; + +import com.sk89q.rebar.command.parametric.ParameterException; + +/** + * Thrown when there are leftover parameters that were not consumed, particular in the + * case of the user providing too many parameters. + */ +public class UnconsumedParameterException extends ParameterException { + + private static final long serialVersionUID = 4449104854894946023L; + + private String unconsumed; + + public UnconsumedParameterException(String unconsumed) { + this.unconsumed = unconsumed; + } + + public String getUnconsumed() { + return unconsumed; + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/binding/PrimitiveBindings.java b/src/main/java/com/sk89q/rebar/command/binding/PrimitiveBindings.java new file mode 100644 index 000000000..f938218c5 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/binding/PrimitiveBindings.java @@ -0,0 +1,258 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.binding; + +import java.lang.annotation.Annotation; + +import com.sk89q.rebar.command.parametric.BindingBehavior; +import com.sk89q.rebar.command.parametric.BindingHelper; +import com.sk89q.rebar.command.parametric.BindingMatch; +import com.sk89q.rebar.command.parametric.ParameterException; +import com.sk89q.rebar.command.parametric.ArgumentStack; + +/** + * Handles basic Java types such as {@link String}s, {@link Byte}s, etc. + * + *

Handles both the object and primitive types.

+ */ +public final class PrimitiveBindings extends BindingHelper { + + /** + * Gets a type from a {@link ArgumentStack}. + * + * @param context the context + * @param text the text annotation + * @param modifiers a list of modifiers + * @return the requested type + * @throws ParameterException on error + */ + @BindingMatch(classifier = Text.class, + type = String.class, + behavior = BindingBehavior.CONSUMES, + consumedCount = -1, + provideModifiers = true) + public String getText(ArgumentStack context, Text text, Annotation[] modifiers) + throws ParameterException { + String v = context.remaining(); + validate(v, modifiers); + return v; + } + + /** + * Gets a type from a {@link ArgumentStack}. + * + * @param context the context + * @param modifiers a list of modifiers + * @return the requested type + * @throws ParameterException on error + */ + @BindingMatch(type = String.class, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1, + provideModifiers = true) + public String getString(ArgumentStack context, Annotation[] modifiers) + throws ParameterException { + String v = context.next(); + validate(v, modifiers); + return v; + } + + /** + * Gets a type from a {@link ArgumentStack}. + * + * @param context the context + * @return the requested type + * @throws ParameterException on error + */ + @BindingMatch(type = { Boolean.class, boolean.class }, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1) + public Boolean getBoolean(ArgumentStack context) throws ParameterException { + return context.nextBoolean(); + } + + /** + * Gets a type from a {@link ArgumentStack}. + * + * @param context the context + * @param modifiers a list of modifiers + * @return the requested type + * @throws ParameterException on error + */ + @BindingMatch(type = { Integer.class, int.class }, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1, + provideModifiers = true) + public Integer getInteger(ArgumentStack context, Annotation[] modifiers) + throws ParameterException { + Integer v = context.nextInt(); + if (v != null) { + validate(v, modifiers); + } + return v; + } + + /** + * Gets a type from a {@link ArgumentStack}. + * + * @param context the context + * @param modifiers a list of modifiers + * @return the requested type + * @throws ParameterException on error + */ + @BindingMatch(type = { Short.class, short.class }, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1, + provideModifiers = true) + public Short getShort(ArgumentStack context, Annotation[] modifiers) + throws ParameterException { + Integer v = getInteger(context, modifiers); + if (v != null) { + return v.shortValue(); + } + return null; + } + + /** + * Gets a type from a {@link ArgumentStack}. + * + * @param context the context + * @param modifiers a list of modifiers + * @return the requested type + * @throws ParameterException on error + */ + @BindingMatch(type = { Double.class, double.class }, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1, + provideModifiers = true) + public Double getDouble(ArgumentStack context, Annotation[] modifiers) + throws ParameterException { + Double v = context.nextDouble(); + if (v != null) { + validate(v, modifiers); + } + return v; + } + + /** + * Gets a type from a {@link ArgumentStack}. + * + * @param context the context + * @param modifiers a list of modifiers + * @return the requested type + * @throws ParameterException on error + */ + @BindingMatch(type = { Float.class, float.class }, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1, + provideModifiers = true) + public Float getFloat(ArgumentStack context, Annotation[] modifiers) + throws ParameterException { + Double v = getDouble(context, modifiers); + if (v != null) { + return v.floatValue(); + } + return null; + } + + /** + * Validate a number value using relevant modifiers. + * + * @param number the number + * @param modifiers the list of modifiers to scan + * @throws ParameterException on a validation error + */ + private static void validate(double number, Annotation[] modifiers) + throws ParameterException { + for (Annotation modifier : modifiers) { + if (modifier instanceof Range) { + Range range = (Range) modifier; + if (number < range.min()) { + throw new ParameterException( + String.format( + "A valid value is greater than or equal to %s " + + "(you entered %s)", range.min(), number)); + } else if (number > range.max()) { + throw new ParameterException( + String.format( + "A valid value is less than or equal to %s " + + "(you entered %s)", range.max(), number)); + } + } + } + } + + /** + * Validate a number value using relevant modifiers. + * + * @param number the number + * @param modifiers the list of modifiers to scan + * @throws ParameterException on a validation error + */ + private static void validate(int number, Annotation[] modifiers) + throws ParameterException { + for (Annotation modifier : modifiers) { + if (modifier instanceof Range) { + Range range = (Range) modifier; + if (number < range.min()) { + throw new ParameterException( + String.format( + "A valid value is greater than or equal to %s " + + "(you entered %s)", range.min(), number)); + } else if (number > range.max()) { + throw new ParameterException( + String.format( + "A valid value is less than or equal to %s " + + "(you entered %s)", range.max(), number)); + } + } + } + } + + /** + * Validate a string value using relevant modifiers. + * + * @param string the string + * @param modifiers the list of modifiers to scan + * @throws ParameterException on a validation error + */ + private static void validate(String string, Annotation[] modifiers) + throws ParameterException { + if (string == null) { + return; + } + + for (Annotation modifier : modifiers) { + if (modifier instanceof Validate) { + Validate validate = (Validate) modifier; + + if (!validate.regex().isEmpty()) { + if (!string.matches(validate.regex())) { + throw new ParameterException( + String.format( + "The given text doesn't match the right " + + "format (technically speaking, the 'format' is %s)", + validate.regex())); + } + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/rebar/command/binding/Range.java b/src/main/java/com/sk89q/rebar/command/binding/Range.java new file mode 100644 index 000000000..67a5eae1b --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/binding/Range.java @@ -0,0 +1,49 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.binding; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies a range of values for numbers. + * + * @see PrimitiveBindings a user of this annotation as a modifier + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Range { + + /** + * The minimum value that the number can be at, inclusive. + * + * @return the minimum value + */ + double min() default Double.MIN_VALUE; + + /** + * The maximum value that the number can be at, inclusive. + * + * @return the maximum value + */ + double max() default Double.MAX_VALUE; + +} diff --git a/src/main/java/com/sk89q/rebar/command/binding/StandardBindings.java b/src/main/java/com/sk89q/rebar/command/binding/StandardBindings.java new file mode 100644 index 000000000..96c82cd22 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/binding/StandardBindings.java @@ -0,0 +1,45 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.binding; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.rebar.command.parametric.BindingBehavior; +import com.sk89q.rebar.command.parametric.BindingHelper; +import com.sk89q.rebar.command.parametric.BindingMatch; +import com.sk89q.rebar.command.parametric.ArgumentStack; + +/** + * Standard bindings that should be available to most configurations. + */ +public final class StandardBindings extends BindingHelper { + + /** + * Gets a {@link CommandContext} from a {@link ArgumentStack}. + * + * @param context the context + * @return a selection + */ + @BindingMatch(type = CommandContext.class, + behavior = BindingBehavior.PROVIDES) + public CommandContext getCommandContext(ArgumentStack context) { + context.markConsumed(); // Consume entire stack + return context.getContext(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/rebar/command/binding/Switch.java b/src/main/java/com/sk89q/rebar/command/binding/Switch.java new file mode 100644 index 000000000..21f8857cb --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/binding/Switch.java @@ -0,0 +1,43 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.binding; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a command flag, such as /command -f. + * + *

If used on a boolean type, then the flag will be a non-value flag. If + * used on any other type, then the flag will be a value flag.

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Switch { + + /** + * The flag character. + * + * @return the flag character (A-Z a-z 0-9 is acceptable) + */ + char value(); + +} diff --git a/src/main/java/com/sk89q/rebar/command/binding/Text.java b/src/main/java/com/sk89q/rebar/command/binding/Text.java new file mode 100644 index 000000000..e8c23e86a --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/binding/Text.java @@ -0,0 +1,40 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.binding; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.sk89q.rebar.command.parametric.ArgumentStack; + +/** + * Indicates a {@link String} parameter will call {@link ArgumentStack#remaining()} and + * therefore consume all remaining arguments. + * + *

This should only be used at the end of a list of parameters (of parameters that + * need to consume from the stack of arguments), otherwise following parameters will + * have no values left to consume.

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Text { + +} diff --git a/src/main/java/com/sk89q/rebar/command/binding/Validate.java b/src/main/java/com/sk89q/rebar/command/binding/Validate.java new file mode 100644 index 000000000..854e3a930 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/binding/Validate.java @@ -0,0 +1,44 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.binding; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.regex.Pattern; + +/** + * Used to validate a string. + * + * @see PrimitiveBindings where this validation is used + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Validate { + + /** + * An optional regular expression that must match the string. + * + * @see Pattern regular expression class + * @return the pattern + */ + String regex() default ""; + +} diff --git a/src/main/java/com/sk89q/rebar/command/fluent/CommandGraph.java b/src/main/java/com/sk89q/rebar/command/fluent/CommandGraph.java new file mode 100644 index 000000000..14d57e75f --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/fluent/CommandGraph.java @@ -0,0 +1,83 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.fluent; + +import com.sk89q.rebar.command.Dispatcher; +import com.sk89q.rebar.command.SimpleDispatcher; +import com.sk89q.rebar.command.parametric.ParametricBuilder; + +/** + * A fluent interface to creating a command graph. + * + *

A command graph may have multiple commands, and multiple sub-commands below that, + * and possibly below that.

+ */ +public class CommandGraph { + + private final DispatcherNode rootDispatcher; + private ParametricBuilder builder; + + /** + * Create a new command graph. + */ + public CommandGraph() { + SimpleDispatcher dispatcher = new SimpleDispatcher(); + rootDispatcher = new DispatcherNode(this, null, dispatcher); + } + + /** + * Get the root dispatcher node. + * + * @return the root dispatcher node + */ + public DispatcherNode commands() { + return rootDispatcher; + } + + /** + * Get the {@link ParametricBuilder}. + * + * @return the builder, or null. + */ + public ParametricBuilder getBuilder() { + return builder; + } + + /** + * Set the {@link ParametricBuilder} used for calls to + * {@link DispatcherNode#build(Object)}. + * + * @param builder the builder, or null + * @return this object + */ + public CommandGraph builder(ParametricBuilder builder) { + this.builder = builder; + return this; + } + + /** + * Get the root dispatcher. + * + * @return the root dispatcher + */ + public Dispatcher getDispatcher() { + return rootDispatcher.getDispatcher(); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/fluent/DispatcherNode.java b/src/main/java/com/sk89q/rebar/command/fluent/DispatcherNode.java new file mode 100644 index 000000000..a17e70964 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/fluent/DispatcherNode.java @@ -0,0 +1,141 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.fluent; + +import com.sk89q.rebar.command.CommandCallable; +import com.sk89q.rebar.command.Dispatcher; +import com.sk89q.rebar.command.SimpleDispatcher; +import com.sk89q.rebar.command.SimpleDispatcherCommand; +import com.sk89q.rebar.command.parametric.ParametricBuilder; + +/** + * A collection of commands. + */ +public class DispatcherNode { + + private final CommandGraph graph; + private final DispatcherNode parent; + private final SimpleDispatcher dispatcher; + + /** + * Create a new instance. + * + * @param graph the root fluent graph object + * @param parent the parent node, or null + * @param dispatcher the dispatcher for this node + */ + DispatcherNode(CommandGraph graph, DispatcherNode parent, + SimpleDispatcher dispatcher) { + this.graph = graph; + this.parent = parent; + this.dispatcher = dispatcher; + } + + /** + * Set the description. + * + *

This can only be used on {@link DispatcherNode}s returned by + * {@link #group(String...)}.

+ * + * @param description the description + * @return this object + */ + public DispatcherNode describe(String description) { + if (dispatcher instanceof SimpleDispatcherCommand) { + ((SimpleDispatcherCommand) dispatcher).getDescription() + .setDescription(description); + } + return this; + } + + /** + * Register a command with this dispatcher. + * + * @param callable the executor + * @param alias the list of aliases, where the first alias is the primary one + */ + public void register(CommandCallable callable, String... alias) { + dispatcher.register(callable, alias); + } + + /** + * Build and register a command with this dispatcher using the + * {@link ParametricBuilder} assigned on the root {@link CommandGraph}. + * + * @param object the object provided to the {@link ParametricBuilder} + * @return this object + * @see ParametricBuilder#register(com.sk89q.rebar.command.Dispatcher, Object) + */ + public DispatcherNode build(Object object) { + ParametricBuilder builder = graph.getBuilder(); + if (builder == null) { + throw new RuntimeException("No ParametricBuilder set"); + } + builder.register(getDispatcher(), object); + return this; + } + + /** + * Create a new command that will contain sub-commands. + * + *

The object returned by this method can be used to add sub-commands. To + * return to this "parent" context, use {@link DispatcherNode#graph()}.

+ * + * @param alias the list of aliases, where the first alias is the primary one + * @return an object to place sub-commands + */ + public DispatcherNode group(String... alias) { + SimpleDispatcherCommand command = new SimpleDispatcherCommand(); + getDispatcher().register(command, alias); + return new DispatcherNode(graph, this, command); + } + + /** + * Return the parent node. + * + * @return the parent node + * @throws RuntimeException if there is no parent node. + */ + public DispatcherNode parent() { + if (parent != null) { + return parent; + } + + throw new RuntimeException("This node does not have a parent"); + } + + /** + * Get the root command graph. + * + * @return the root command graph + */ + public CommandGraph graph() { + return graph; + } + + /** + * Get the underlying dispatcher of this object. + * + * @return the dispatcher + */ + public Dispatcher getDispatcher() { + return dispatcher; + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/AbstractInvokeListener.java b/src/main/java/com/sk89q/rebar/command/parametric/AbstractInvokeListener.java new file mode 100644 index 000000000..a03550b6d --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/AbstractInvokeListener.java @@ -0,0 +1,35 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.Method; + +import com.sk89q.rebar.command.SimpleDescription; + +/** + * An abstract listener. + */ +public abstract class AbstractInvokeListener implements InvokeListener { + + @Override + public void updateDescription(Object object, Method method, + ParameterData[] parameters, SimpleDescription description) { + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ArgumentStack.java b/src/main/java/com/sk89q/rebar/command/parametric/ArgumentStack.java new file mode 100644 index 000000000..221002e39 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ArgumentStack.java @@ -0,0 +1,77 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ + +package com.sk89q.rebar.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandContext; + +public interface ArgumentStack { + + /** + * Get the next string, which may come from the stack or a value flag. + * + * @return the value + * @throws ParameterException on a parameter error + */ + String next() throws ParameterException; + + /** + * Get the next integer, which may come from the stack or a value flag. + * + * @return the value + * @throws ParameterException on a parameter error + */ + Integer nextInt() throws ParameterException; + + /** + * Get the next double, which may come from the stack or a value flag. + * + * @return the value + * @throws ParameterException on a parameter error + */ + Double nextDouble() throws ParameterException; + + /** + * Get the next boolean, which may come from the stack or a value flag. + * + * @return the value + * @throws ParameterException on a parameter error + */ + Boolean nextBoolean() throws ParameterException; + + /** + * Get all remaining string values, which will consume the rest of the stack. + * + * @return the value + * @throws ParameterException on a parameter error + */ + String remaining() throws ParameterException; + + /** + * Set as completely consumed. + */ + void markConsumed(); + + /** + * Get the underlying context. + * + * @return the context + */ + CommandContext getContext(); + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/rebar/command/parametric/Binding.java b/src/main/java/com/sk89q/rebar/command/parametric/Binding.java new file mode 100644 index 000000000..6234a6db3 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/Binding.java @@ -0,0 +1,91 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.Type; +import java.util.List; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.rebar.command.binding.PrimitiveBindings; +import com.sk89q.rebar.command.binding.StandardBindings; + +/** + * Used to parse user input for a command, based on available method types + * and annotations. + * + *

A binding can be used to handle several types at once. For a binding to be + * called, it must be registered with a {@link ParametricBuilder} with + * {@link ParametricBuilder#addBinding(Binding, java.lang.reflect.Type...)}.

+ * + * @see PrimitiveBindings an example of primitive bindings + * @see StandardBindings standard bindings + */ +public interface Binding { + + /** + * Get the types that this binding handles. + * + * @return the types + */ + Type[] getTypes(); + + /** + * Get how this binding consumes from a {@link ArgumentStack}. + * + * @param parameter information about the parameter + * @return the behavior + */ + BindingBehavior getBehavior(ParameterData parameter); + + /** + * Get the number of arguments that this binding will consume, if this + * information is available. + * + *

This method must return -1 for binding behavior types that are not + * {@link BindingBehavior#CONSUMES}.

+ * + * @param parameter information about the parameter + * @return the number of consumed arguments, or -1 if unknown or irrelevant + */ + int getConsumedCount(ParameterData parameter); + + /** + * Attempt to consume values (if required) from the given {@link ArgumentStack} + * in order to instantiate an object for the given parameter. + * + * @param parameter information about the parameter + * @param scoped the arguments the user has input + * @param onlyConsume true to only consume arguments + * @return an object parsed for the given parameter + * @throws ParameterException thrown if the parameter could not be formulated + * @throws CommandException on a command exception + */ + Object bind(ParameterData parameter, ArgumentStack scoped, boolean onlyConsume) + throws ParameterException, CommandException; + + /** + * Get a list of suggestions for the given parameter and user arguments. + * + * @param parameter information about the parameter + * @param prefix what the user has typed so far (may be an empty string) + * @return a list of suggestions + */ + List getSuggestions(ParameterData parameter, String prefix); + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/BindingBehavior.java b/src/main/java/com/sk89q/rebar/command/parametric/BindingBehavior.java new file mode 100644 index 000000000..b15b5bd03 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/BindingBehavior.java @@ -0,0 +1,51 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandLocals; +import com.sk89q.rebar.command.binding.Switch; + +/** + * Determines the type of binding. + */ +public enum BindingBehavior { + + /** + * Always consumes from a {@link ArgumentStack}. + */ + CONSUMES, + + /** + * Sometimes consumes from a {@link ArgumentStack}. + * + *

Bindings that exhibit this behavior must be defined as a {@link Switch} + * by commands utilizing the given binding.

+ */ + INDETERMINATE, + + /** + * Never consumes from a {@link ArgumentStack}. + * + *

Bindings that exhibit this behavior generate objects from other sources, + * such as from a {@link CommandLocals}. These are "magic" bindings that inject + * variables.

+ */ + PROVIDES; + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/BindingHelper.java b/src/main/java/com/sk89q/rebar/command/parametric/BindingHelper.java new file mode 100644 index 000000000..b3bc4aad0 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/BindingHelper.java @@ -0,0 +1,223 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.sk89q.minecraft.util.commands.CommandException; + +/** + * A binding helper that uses the {@link BindingMatch} annotation to make + * writing bindings extremely easy. + * + *

Methods must have the following and only the following parameters:

+ * + *
    + *
  • A {@link ArgumentStack}
  • + *
  • A {@link Annotation} if there is a classifier set
  • + *
  • A {@link Annotation}[] + * if there {@link BindingMatch#provideModifiers()} is true
  • + *
+ * + *

Methods may throw any exception. Exceptions may be converted using a + * {@link ExceptionConverter} registered with the {@link ParametricBuilder}.

+ */ +public class BindingHelper implements Binding { + + private final List bindings; + private final Type[] types; + + /** + * Create a new instance. + */ + public BindingHelper() { + List bindings = new ArrayList(); + List types = new ArrayList(); + + for (Method method : this.getClass().getMethods()) { + BindingMatch info = method.getAnnotation(BindingMatch.class); + if (info != null) { + Class classifier = null; + + // Set classifier + if (!info.classifier().equals(Annotation.class)) { + classifier = (Class) info.classifier(); + types.add(classifier); + } + + for (Type t : info.type()) { + Type type = null; + + // Set type + if (!t.equals(Class.class)) { + type = t; + if (classifier == null) { + types.add(type); // Only if there is no classifier set! + } + } + + // Check to see if at least one is set + if (type == null && classifier == null) { + throw new RuntimeException( + "A @BindingMatch needs either a type or classifier set"); + } + + BoundMethod handler = new BoundMethod(info, type, classifier, method); + bindings.add(handler); + } + } + } + + Collections.sort(bindings); + + this.bindings = bindings; + + Type[] typesArray = new Type[types.size()]; + types.toArray(typesArray); + this.types = typesArray; + + } + + /** + * Match a {@link BindingMatch} according to the given parameter. + * + * @param parameter the parameter + * @return a binding + */ + private BoundMethod match(ParameterData parameter) { + for (BoundMethod binding : bindings) { + Annotation classifer = parameter.getClassifier(); + Type type = parameter.getType(); + + if (binding.classifier != null) { + if (classifer != null && classifer.annotationType().equals(binding.classifier)) { + if (binding.type == null || binding.type.equals(type)) { + return binding; + } + } + } else if (binding.type.equals(type)) { + return binding; + } + } + + throw new RuntimeException("Unknown type"); + } + + @Override + public Type[] getTypes() { + return types; + } + + @Override + public int getConsumedCount(ParameterData parameter) { + return match(parameter).annotation.consumedCount(); + } + + @Override + public BindingBehavior getBehavior(ParameterData parameter) { + return match(parameter).annotation.behavior(); + } + + @Override + public Object bind(ParameterData parameter, ArgumentStack scoped, + boolean onlyConsume) throws ParameterException, CommandException { + BoundMethod binding = match(parameter); + List args = new ArrayList(); + args.add(scoped); + + if (binding.classifier != null) { + args.add(parameter.getClassifier()); + } + + if (binding.annotation.provideModifiers()) { + args.add(parameter.getModifiers()); + } + + if (onlyConsume && binding.annotation.behavior() == BindingBehavior.PROVIDES) { + return null; // Nothing to consume, nothing to do + } + + Object[] argsArray = new Object[args.size()]; + args.toArray(argsArray); + + try { + return binding.method.invoke(this, argsArray); + } catch (IllegalArgumentException e) { + throw new RuntimeException( + "Processing of classifier " + parameter.getClassifier() + + " and type " + parameter.getType() + " failed for method\n" + + binding.method + "\nbecause the parameters for that method are wrong", e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof ParameterException) { + throw (ParameterException) e.getCause(); + } else if (e.getCause() instanceof CommandException) { + throw (CommandException) e.getCause(); + } + throw new ParameterException(e.getCause()); + } + } + + @Override + public List getSuggestions(ParameterData parameter, String prefix) { + return new ArrayList(); + } + + private static class BoundMethod implements Comparable { + private final BindingMatch annotation; + private final Type type; + private final Class classifier; + private final Method method; + + BoundMethod(BindingMatch annotation, Type type, + Class classifier, Method method) { + this.annotation = annotation; + this.type = type; + this.classifier = classifier; + this.method = method; + } + + @Override + public int compareTo(BoundMethod o) { + if (classifier != null && o.classifier == null) { + return -1; + } else if (classifier == null && o.classifier != null) { + return 1; + } else if (classifier != null && o.classifier != null) { + if (type != null && o.type == null) { + return -1; + } else if (type == null && o.type != null) { + return 1; + } else { + return 0; + } + } else { + return 0; + } + } + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/BindingMatch.java b/src/main/java/com/sk89q/rebar/command/parametric/BindingMatch.java new file mode 100644 index 000000000..5cbc34ffb --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/BindingMatch.java @@ -0,0 +1,70 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Denotes a match of a binding. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface BindingMatch { + + /** + * The classifier. + * + * @return the classifier, or {@link Annotation} if not set + */ + Class classifier() default Annotation.class; + + /** + * The type. + * + * @return the type, or {@link Class} if not set + */ + Class[] type() default Class.class; + + /** + * The binding behavior. + * + * @return the behavior + */ + BindingBehavior behavior(); + + /** + * Get the number of arguments that this binding consumes. + * + * @return -1 if unknown or irrelevant + */ + int consumedCount() default -1; + + /** + * Set whether an array of modifier annotations is provided in the list of + * arguments. + * + * @return true to provide modifiers + */ + boolean provideModifiers() default false; + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ContextArgumentStack.java b/src/main/java/com/sk89q/rebar/command/parametric/ContextArgumentStack.java new file mode 100644 index 000000000..182ecf6c0 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ContextArgumentStack.java @@ -0,0 +1,177 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.rebar.command.MissingParameterException; + +/** + * Makes an instance of a {@link CommandContext} into a stack of arguments + * that can be consumed. + * + * @see ParametricBuilder a user of this class + */ +public class ContextArgumentStack implements ArgumentStack { + + private final CommandContext context; + private int index = 0; + private int markedIndex = 0; + + /** + * Create a new instance using the given context. + * + * @param context the context + */ + public ContextArgumentStack(CommandContext context) { + this.context = context; + } + + @Override + public String next() throws ParameterException { + try { + return context.getString(index++); + } catch (IndexOutOfBoundsException e) { + throw new MissingParameterException(); + } + } + + @Override + public Integer nextInt() throws ParameterException { + try { + return Integer.parseInt(next()); + } catch (NumberFormatException e) { + throw new ParameterException( + "Expected a number, got '" + context.getString(index - 1) + "'"); + } + } + + @Override + public Double nextDouble() throws ParameterException { + try { + return Double.parseDouble(next()); + } catch (NumberFormatException e) { + throw new ParameterException( + "Expected a number, got '" + context.getString(index - 1) + "'"); + } + } + + @Override + public Boolean nextBoolean() throws ParameterException { + try { + return next().equalsIgnoreCase("true"); + } catch (IndexOutOfBoundsException e) { + throw new MissingParameterException(); + } + } + + @Override + public String remaining() throws ParameterException { + try { + String value = context.getJoinedStrings(index); + index = context.argsLength(); + return value; + } catch (IndexOutOfBoundsException e) { + throw new MissingParameterException(); + } + } + + /** + * Get the unconsumed arguments left over, without touching the stack. + * + * @return the unconsumed arguments + */ + public String getUnconsumed() { + if (index >= context.argsLength()) { + return null; + } + + return context.getJoinedStrings(index); + } + + @Override + public void markConsumed() { + index = context.argsLength(); + } + + /** + * Return the current position. + * + * @return the position + */ + public int position() { + return index; + } + + /** + * Mark the current position of the stack. + * + *

The marked position initially starts at 0.

+ */ + public void mark() { + markedIndex = index; + } + + /** + * Reset to the previously {@link #mark()}ed position of the stack, and return + * the arguments that were consumed between this point and that previous point. + * + *

The marked position initially starts at 0.

+ * + * @return the consumed arguments + */ + public String reset() { + String value = context.getString(markedIndex, index); + index = markedIndex; + return value; + } + + /** + * Return whether any arguments were consumed between the marked position + * and the current position. + * + *

The marked position initially starts at 0.

+ * + * @return true if values were consumed. + */ + public boolean wasConsumed() { + return markedIndex != index; + } + + /** + * Return the arguments that were consumed between this point and that marked point. + * + *

The marked position initially starts at 0.

+ * + * @return the consumed arguments + */ + public String getConsumed() { + return context.getString(markedIndex, index); + } + + /** + * Get the underlying context. + * + * @return the context + */ + @Override + public CommandContext getContext() { + return context; + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverter.java b/src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverter.java new file mode 100644 index 000000000..d40a643db --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverter.java @@ -0,0 +1,51 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.WrappedCommandException; + +/** + * Used to convert a recognized {@link Throwable} into an appropriate + * {@link CommandException}. + * + *

Methods (when invoked by a {@link ParametricBuilder}-created command) may throw + * relevant exceptions that are not caught by the command manager, but translate + * into reasonable exceptions for an application. However, unknown exceptions are + * normally simply wrapped in a {@link WrappedCommandException} and bubbled up. Only + * normal {@link CommandException}s will be printed correctly, so a converter translates + * one of these unknown exceptions into an appropriate {@link CommandException}.

+ * + *

This also allows the code calling the command to not need be aware of these + * application-specific exceptions, as they will all be converted to + * {@link CommandException}s that are handled normally.

+ */ +public interface ExceptionConverter { + + /** + * Attempt to convert the given throwable into a {@link CommandException}. + * + *

If the exception is not recognized, then nothing should be thrown.

+ * + * @param t the throwable + * @throws CommandException a command exception + */ + void convert(Throwable t) throws CommandException; + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverterHelper.java b/src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverterHelper.java new file mode 100644 index 000000000..a0e84185f --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ExceptionConverterHelper.java @@ -0,0 +1,108 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.WrappedCommandException; + +/** + * An implementation of an {@link ExceptionConverter} that automatically calls + * the correct method defined on this object. + * + *

Only public methods will be used. Methods will be called in order of decreasing + * levels of inheritance (between classes where one inherits the other). For two + * different inheritance branches, the order between them is undefined.

+ */ +public abstract class ExceptionConverterHelper implements ExceptionConverter { + + private final List handlers; + + @SuppressWarnings("unchecked") + public ExceptionConverterHelper() { + List handlers = new ArrayList(); + + for (Method method : this.getClass().getMethods()) { + if (method.getAnnotation(ExceptionMatch.class) == null) { + continue; + } + + Class[] parameters = method.getParameterTypes(); + if (parameters.length == 1) { + Class cls = parameters[0]; + if (Throwable.class.isAssignableFrom(cls)) { + handlers.add(new ExceptionHandler( + (Class) cls, method)); + } + } + } + + Collections.sort(handlers); + + this.handlers = handlers; + } + + @Override + public void convert(Throwable t) throws CommandException { + Class throwableClass = t.getClass(); + for (ExceptionHandler handler : handlers) { + if (handler.cls.isAssignableFrom(throwableClass)) { + try { + handler.method.invoke(this, t); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof CommandException) { + throw (CommandException) e.getCause(); + } + throw new WrappedCommandException(e); + } catch (IllegalArgumentException e) { + throw new WrappedCommandException(e); + } catch (IllegalAccessException e) { + throw new WrappedCommandException(e); + } + } + } + } + + private static class ExceptionHandler implements Comparable { + final Class cls; + final Method method; + + public ExceptionHandler(Class cls, Method method) { + this.cls = cls; + this.method = method; + } + + @Override + public int compareTo(ExceptionHandler o) { + if (cls.equals(o.cls)) { + return 0; + } else if (cls.isAssignableFrom(o.cls)) { + return 1; + } else { + return -1; + } + } + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ExceptionMatch.java b/src/main/java/com/sk89q/rebar/command/parametric/ExceptionMatch.java new file mode 100644 index 000000000..5c3dae02d --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ExceptionMatch.java @@ -0,0 +1,33 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Denotes a match of an exception. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExceptionMatch { + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/InvokeHandler.java b/src/main/java/com/sk89q/rebar/command/parametric/InvokeHandler.java new file mode 100644 index 000000000..f27ac1759 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/InvokeHandler.java @@ -0,0 +1,81 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.Method; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; + +/** + * Called before and after a command is invoked for commands executed by a command + * created using {@link ParametricBuilder}. + * + *

Invocation handlers are created by {@link InvokeListener}s. Multiple + * listeners and handlers can be registered, and all be run. However, if one handler + * throws an exception, future handlers will not execute and the command will + * not execute (if thrown in + * {@link #preInvoke(Object, Method, ParameterData[], Object[], CommandContext)}).

+ * + * @see InvokeListener the factory + */ +public interface InvokeHandler { + + /** + * Called before parameters are processed. + * + * @param object the object + * @param method the method + * @param parameters the list of parameters + * @param context the context + * @throws CommandException can be thrown for an error, which will stop invocation + * @throws ParameterException on parameter error + */ + void preProcess(Object object, Method method, ParameterData[] parameters, + CommandContext context) throws CommandException, ParameterException; + + /** + * Called before the parameter is invoked. + * + * @param object the object + * @param method the method + * @param parameters the list of parameters + * @param args the arguments to be given to the method + * @param context the context + * @throws CommandException can be thrown for an error, which will stop invocation + * @throws ParameterException on parameter error + */ + void preInvoke(Object object, Method method, ParameterData[] parameters, + Object[] args, CommandContext context) throws CommandException, ParameterException; + + /** + * Called after the parameter is invoked. + * + * @param object the object + * @param method the method + * @param parameters the list of parameters + * @param args the arguments to be given to the method + * @param context the context + * @throws CommandException can be thrown for an error + * @throws ParameterException on parameter error + */ + void postInvoke(Object object, Method method, ParameterData[] parameters, + Object[] args, CommandContext context) throws CommandException, ParameterException; + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/InvokeListener.java b/src/main/java/com/sk89q/rebar/command/parametric/InvokeListener.java new file mode 100644 index 000000000..2e0898ab1 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/InvokeListener.java @@ -0,0 +1,57 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.Method; + +import com.sk89q.minecraft.util.commands.CommandPermissions; +import com.sk89q.rebar.command.CommandCallable; +import com.sk89q.rebar.command.SimpleDescription; + +/** + * Listens to events related to {@link ParametricBuilder}. + */ +public interface InvokeListener { + + /** + * Create a new invocation handler. + * + *

An example use of an {@link InvokeHandler} would be to verify permissions + * added by the {@link CommandPermissions} annotation.

+ * + *

For simple {@link InvokeHandler}, an object can implement both this + * interface and {@link InvokeHandler}.

+ * + * @return a new invocation handler + */ + InvokeHandler createInvokeHandler(); + + /** + * During creation of a {@link CommandCallable} by a {@link ParametricBuilder}, + * this will be called in case the description needs to be updated. + * + * @param object the object + * @param method the method + * @param parameters a list of parameters + * @param description the description to be updated + */ + void updateDescription(Object object, Method method, ParameterData[] parameters, + SimpleDescription description); + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/LegacyCommandsHandler.java b/src/main/java/com/sk89q/rebar/command/parametric/LegacyCommandsHandler.java new file mode 100644 index 000000000..944077fb3 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/LegacyCommandsHandler.java @@ -0,0 +1,96 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.Method; + +import com.sk89q.minecraft.util.commands.Command; +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.rebar.command.MissingParameterException; +import com.sk89q.rebar.command.SimpleDescription; +import com.sk89q.rebar.command.UnconsumedParameterException; + +/** + * Handles legacy properties on {@link Command} such as {@link Command#min()} and + * {@link Command#max()}. + */ +public class LegacyCommandsHandler extends AbstractInvokeListener implements InvokeHandler { + + @Override + public InvokeHandler createInvokeHandler() { + return this; + } + + @Override + public void preProcess(Object object, Method method, + ParameterData[] parameters, CommandContext context) + throws CommandException, ParameterException { + } + + @Override + public void preInvoke(Object object, Method method, + ParameterData[] parameters, Object[] args, CommandContext context) + throws ParameterException { + Command annotation = method.getAnnotation(Command.class); + + if (annotation != null) { + if (context.argsLength() < annotation.min()) { + throw new MissingParameterException(); + } + + if (annotation.max() != -1 && context.argsLength() > annotation.max()) { + throw new UnconsumedParameterException( + context.getRemainingString(annotation.max())); + } + } + } + + @Override + public void postInvoke(Object object, Method method, + ParameterData[] parameters, Object[] args, CommandContext context) { + + } + + @Override + public void updateDescription(Object object, Method method, + ParameterData[] parameters, SimpleDescription description) { + Command annotation = method.getAnnotation(Command.class); + + // Handle the case for old commands where no usage is set and all of its + // parameters are provider bindings, so its usage information would + // be blank and would imply that there were no accepted parameters + if (annotation != null && annotation.usage().isEmpty() + && (annotation.min() > 0 || annotation.max() > 0)) { + boolean hasUserParameters = false; + + for (ParameterData parameter : parameters) { + if (parameter.getBinding().getBehavior(parameter) != BindingBehavior.PROVIDES) { + hasUserParameters = true; + return; + } + } + + if (!hasUserParameters) { + description.overrideUsage("(unknown usage information)"); + } + } + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/Optional.java b/src/main/java/com/sk89q/rebar/command/parametric/Optional.java new file mode 100644 index 000000000..7e0c075af --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/Optional.java @@ -0,0 +1,40 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates an optional parameter. + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Optional { + + /** + * The default value to use if no value is set. + * + * @return a string value, or an empty list + */ + String[] value() default {}; + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ParameterData.java b/src/main/java/com/sk89q/rebar/command/parametric/ParameterData.java new file mode 100644 index 000000000..42038051e --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ParameterData.java @@ -0,0 +1,193 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +import com.sk89q.rebar.command.SimpleParameter; +import com.sk89q.rebar.command.binding.PrimitiveBindings; +import com.sk89q.rebar.command.binding.Range; +import com.sk89q.rebar.command.binding.Text; + +/** + * Describes a parameter in detail. + */ +public class ParameterData extends SimpleParameter { + + private Binding binding; + private Annotation classifier; + private Annotation[] modifiers; + private Type type; + + /** + * Get the binding associated with this parameter. + * + * @return the binding + */ + public Binding getBinding() { + return binding; + } + + /** + * Set the binding associated with this parameter. + * + * @param binding the binding + */ + void setBinding(Binding binding) { + this.binding = binding; + } + + /** + * Set the main type of this parameter. + * + *

The type is normally that is used to determine which binding is used + * for a particular method's parameter.

+ * + * @return the main type + * @see #getClassifier() which can override the type + */ + public Type getType() { + return type; + } + + /** + * Set the main type of this parameter. + * + * @param type the main type + */ + void setType(Type type) { + this.type = type; + } + + /** + * Get the classifier annotation. + * + *

Normally, the type determines what binding is called, but classifiers + * take precedence if one is found (and registered with + * {@link ParametricBuilder#addBinding(Binding, Type...)}). + * An example of a classifier annotation is {@link Text}.

+ * + * @return the classifier annotation, null is possible + */ + public Annotation getClassifier() { + return classifier; + } + + /** + * Set the classifier annotation. + * + * @param classifier the classifier annotation, null is possible + */ + void setClassifier(Annotation classifier) { + this.classifier = classifier; + } + + /** + * Get a list of modifier annotations. + * + *

Modifier annotations are not considered in the process of choosing a binding + * for a method parameter, but they can be used to modify the behavior of a binding. + * An example of a modifier annotation is {@link Range}, which can restrict + * numeric values handled by {@link PrimitiveBindings} to be within a range. The list + * of annotations may contain a classifier and other unrelated annotations.

+ * + * @return a list of annotations + */ + public Annotation[] getModifiers() { + return modifiers; + } + + /** + * Set the list of modifiers. + * + * @param modifiers a list of annotations + */ + void setModifiers(Annotation[] modifiers) { + this.modifiers = modifiers; + } + + /** + * Return the number of arguments this binding consumes. + * + * @return -1 if unknown or unavailable + */ + int getConsumedCount() { + return getBinding().getConsumedCount(this); + } + + /** + * Get whether this parameter is entered by the user. + * + * @return true if this parameter is entered by the user. + */ + boolean isUserInput() { + return getBinding().getBehavior(this) != BindingBehavior.PROVIDES; + } + + /** + * Get whether this parameter consumes non-flag arguments. + * + * @return true if this parameter consumes non-flag arguments + */ + boolean isNonFlagConsumer() { + return getBinding().getBehavior(this) != BindingBehavior.PROVIDES && !isValueFlag(); + } + + /** + * Validate this parameter and its binding. + */ + void validate(Method method, int parameterIndex) throws ParametricException { + // We can't have indeterminate consumers without @Switches otherwise + // it may screw up parameter processing for later bindings + BindingBehavior behavior = getBinding().getBehavior(this); + boolean indeterminate = (behavior == BindingBehavior.INDETERMINATE); + if (!isValueFlag() && indeterminate) { + throw new ParametricException( + "@Switch missing for indeterminate consumer\n\n" + + "Notably:\nFor the type " + type + ", the binding " + + getBinding().getClass().getCanonicalName() + + "\nmay or may not consume parameters (isIndeterminateConsumer(" + type + ") = true)" + + "\nand therefore @Switch(flag) is required for parameter #" + parameterIndex + " of \n" + + method.toGenericString()); + } + + // getConsumedCount() better return -1 if the BindingBehavior is not CONSUMES + if (behavior != BindingBehavior.CONSUMES && binding.getConsumedCount(this) != -1) { + throw new ParametricException( + "getConsumedCount() does not return -1 for binding " + + getBinding().getClass().getCanonicalName() + + "\neven though its behavior type is " + behavior.name() + + "\nfor parameter #" + parameterIndex + " of \n" + + method.toGenericString()); + } + + // getConsumedCount() should not return 0 if the BindingBehavior is not PROVIDES + if (behavior != BindingBehavior.PROVIDES && binding.getConsumedCount(this) == 0) { + throw new ParametricException( + "getConsumedCount() must not return 0 for binding " + + getBinding().getClass().getCanonicalName() + + "\nwhen its behavior type is " + behavior.name() + " and not PROVIDES " + + "\nfor parameter #" + parameterIndex + " of \n" + + method.toGenericString()); + } + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ParameterException.java b/src/main/java/com/sk89q/rebar/command/parametric/ParameterException.java new file mode 100644 index 000000000..6851d36a1 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ParameterException.java @@ -0,0 +1,44 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +/** + * Thrown if there is an error with a parameter. + */ +public class ParameterException extends Exception { + + private static final long serialVersionUID = -8255175019708245673L; + + public ParameterException() { + super(); + } + + public ParameterException(String message) { + super(message); + } + + public ParameterException(Throwable cause) { + super(cause); + } + + public ParameterException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ParametricBuilder.java b/src/main/java/com/sk89q/rebar/command/parametric/ParametricBuilder.java new file mode 100644 index 000000000..4faa4c6f4 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ParametricBuilder.java @@ -0,0 +1,204 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableBiMap.Builder; +import com.sk89q.minecraft.util.commands.Command; +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.CommandPermissions; +import com.sk89q.rebar.command.CommandCallable; +import com.sk89q.rebar.command.Dispatcher; +import com.sk89q.rebar.command.binding.PrimitiveBindings; +import com.sk89q.rebar.command.binding.StandardBindings; +import com.sk89q.rebar.command.binding.Switch; +import com.thoughtworks.paranamer.CachingParanamer; +import com.thoughtworks.paranamer.Paranamer; + +/** + * Creates commands using annotations placed on methods and individual parameters of + * such methods. + * + * @see Command defines a command + * @see Switch defines a flag + */ +public class ParametricBuilder { + + private final Map bindings = new HashMap(); + private final Paranamer paranamer = new CachingParanamer(); + private final List invokeListeners = new ArrayList(); + private final List exceptionConverters = new ArrayList(); + + /** + * Create a new builder. + * + *

This method will install {@link PrimitiveBindings} and + * {@link StandardBindings} and default bindings.

+ */ + public ParametricBuilder() { + addBinding(new PrimitiveBindings()); + addBinding(new StandardBindings()); + } + + /** + * Add a binding for a given type or classifier (annotation). + * + *

Whenever a method parameter is encountered, a binding must be found for it + * so that it can be called later to consume the stack of arguments provided by + * the user and return an object that is later passed to + * {@link Method#invoke(Object, Object...)}.

+ * + *

Normally, a {@link Type} is used to discern between different bindings, but + * if this is not specific enough, an annotation can be defined and used. This + * makes it a "classifier" and it will take precedence over the base type. For + * example, even if there is a binding that handles {@link String} parameters, + * a special @MyArg annotation can be assigned to a {@link String} + * parameter, which will cause the {@link Builder} to consult the {@link Binding} + * associated with @MyArg rather than with the binding for + * the {@link String} type.

+ * + * @param binding the binding + * @param type a list of types (if specified) to override the binding's types + */ + public void addBinding(Binding binding, Type... type) { + if (type == null || type.length == 0) { + type = binding.getTypes(); + } + + for (Type t : type) { + bindings.put(t, binding); + } + } + + /** + * Attach an invocation listener. + * + *

Invocation handlers are called in order that their listeners are + * registered with a {@link ParametricBuilder}. It is not guaranteed that + * a listener may be called, in the case of a {@link CommandException} being + * thrown at any time before the appropriate listener or handler is called. + * It is possible for a + * {@link InvokeHandler#preInvoke(Object, Method, ParameterData[], Object[], CommandContext)} to + * be called for a invocation handler, but not the associated + * {@link InvokeHandler#postInvoke(Object, Method, ParameterData[], Object[], CommandContext)}.

+ * + *

An example of an invocation listener is one to handle + * {@link CommandPermissions}, by first checking to see if permission is available + * in a {@link InvokeHandler#preInvoke(Object, Method, ParameterData[], Object[], CommandContext)} + * call. If permission is not found, then an appropriate {@link CommandException} + * can be thrown to cease invocation.

+ * + * @param listener the listener + * @see InvokeHandler the handler + */ + public void attach(InvokeListener listener) { + invokeListeners.add(listener); + } + + /** + * Attach an exception converter to this builder in order to wrap unknown + * {@link Throwable}s into known {@link CommandException}s. + * + *

Exception converters are called in order that they are registered.

+ * + * @param converter the converter + * @see ExceptionConverter for an explanation + */ + public void attach(ExceptionConverter converter) { + exceptionConverters.add(converter); + } + + /** + * Build a list of commands from methods specially annotated with {@link Command} + * (and other relevant annotations) and register them all with the given + * {@link Dispatcher}. + * + * @param dispatcher the dispatcher to register commands with + * @param object the object contain the methods + * @throws ParametricException thrown if the commands cannot be registered + */ + public void register(Dispatcher dispatcher, Object object) throws ParametricException { + for (Method method : object.getClass().getDeclaredMethods()) { + Command definition = method.getAnnotation(Command.class); + if (definition != null) { + CommandCallable callable = build(object, method, definition); + dispatcher.register(callable, definition.aliases()); + } + } + } + + /** + * Build a {@link CommandCallable} for the given method. + * + * @param object the object to be invoked on + * @param method the method to invoke + * @param definition the command definition annotation + * @return the command executor + * @throws ParametricException thrown on an error + */ + private CommandCallable build(Object object, Method method, Command definition) + throws ParametricException { + return new ParametricCallable(this, object, method, definition); + } + + /** + * Get the object used to get method names on Java versions before 8 (assuming + * that Java 8 is given the ability to reliably reflect method names at runtime). + * + * @return the paranamer + */ + Paranamer getParanamer() { + return paranamer; + } + + /** + * Get the map of bindings. + * + * @return the map of bindings + */ + Map getBindings() { + return bindings; + } + + /** + * Get a list of invocation listeners. + * + * @return a list of invocation listeners + */ + List getInvokeListeners() { + return invokeListeners; + } + + /** + * Get the list of exception converters. + * + * @return a list of exception converters + */ + List getExceptionConverters() { + return exceptionConverters; + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ParametricCallable.java b/src/main/java/com/sk89q/rebar/command/parametric/ParametricCallable.java new file mode 100644 index 000000000..47d76ed4c --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ParametricCallable.java @@ -0,0 +1,537 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.sk89q.minecraft.util.commands.Command; +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.CommandPermissions; +import com.sk89q.minecraft.util.commands.SuggestionContext; +import com.sk89q.minecraft.util.commands.WrappedCommandException; +import com.sk89q.rebar.command.CommandCallable; +import com.sk89q.rebar.command.InvalidUsageException; +import com.sk89q.rebar.command.MissingParameterException; +import com.sk89q.rebar.command.Parameter; +import com.sk89q.rebar.command.SimpleDescription; +import com.sk89q.rebar.command.UnconsumedParameterException; +import com.sk89q.rebar.command.binding.Switch; + +/** + * The implementation of a {@link CommandCallable} for the {@link ParametricBuilder}. + */ +class ParametricCallable implements CommandCallable { + + private final ParametricBuilder builder; + private final Object object; + private final Method method; + private final ParameterData[] parameters; + private final Set valueFlags = new HashSet(); + private final SimpleDescription description = new SimpleDescription(); + + /** + * Create a new instance. + * + * @param builder the parametric builder + * @param object the object to invoke on + * @param method the method to invoke + * @param definition the command definition annotation + * @throws ParametricException thrown on an error + */ + ParametricCallable( + ParametricBuilder builder, + Object object, Method method, + Command definition) throws ParametricException { + + this.builder = builder; + this.object = object; + this.method = method; + + Annotation[][] annotations = method.getParameterAnnotations(); + String[] names = builder.getParanamer().lookupParameterNames(method, false); + Type[] types = method.getGenericParameterTypes(); + parameters = new ParameterData[types.length]; + List userParameters = new ArrayList(); + + // This helps keep tracks of @Nullables that appear in the middle of a list + // of parameters + int numOptional = 0; + + // Set permission hint + CommandPermissions permHint = method.getAnnotation(CommandPermissions.class); + if (permHint != null) { + description.setPermissions(Arrays.asList(permHint.value())); + } + + // Go through each parameter + for (int i = 0; i < types.length; i++) { + Type type = types[i]; + + ParameterData parameter = new ParameterData(); + parameter.setType(type); + parameter.setModifiers(annotations[i]); + + // Search for annotations + for (Annotation annotation : annotations[i]) { + if (annotation instanceof Switch) { + parameter.setFlag(((Switch) annotation).value(), type != boolean.class); + } else if (annotation instanceof Nullable) { + parameter.setOptional(true); + } else if (annotation instanceof Optional) { + parameter.setOptional(true); + String[] value = ((Optional) annotation).value(); + if (value.length > 0) { + parameter.setDefaultValue(value); + } + // Special annotation bindings + } else if (parameter.getBinding() == null) { + parameter.setBinding(builder.getBindings().get( + annotation.annotationType())); + parameter.setClassifier(annotation); + } + } + + parameter.setName(names.length > 0 ? + names[i] : generateName(type, parameter.getClassifier(), i)); + + // Track all value flags + if (parameter.isValueFlag()) { + valueFlags.add(parameter.getFlag()); + } + + // No special @annotation binding... let's check for the type + if (parameter.getBinding() == null) { + parameter.setBinding(builder.getBindings().get(type)); + + // Don't know how to parse for this type of value + if (parameter.getBinding() == null) { + throw new ParametricException( + "Don't know how to handle the parameter type '" + type + "' in\n" + + method.toGenericString()); + } + } + + // Do some validation of this parameter + parameter.validate(method, i + 1); + + // Keep track of optional parameters + if (parameter.isOptional() && parameter.getFlag() == null) { + numOptional++; + } else { + if (numOptional > 0 && parameter.isNonFlagConsumer()) { + if (parameter.getConsumedCount() < 0) { + throw new ParametricException( + "Found an parameter using the binding " + + parameter.getBinding().getClass().getCanonicalName() + + "\nthat does not know how many arguments it consumes, but " + + "it follows an optional parameter\nMethod: " + + method.toGenericString()); + } + } + } + + parameters[i] = parameter; + + // Make a list of "real" parameters + if (parameter.isUserInput()) { + userParameters.add(parameter); + } + } + + // Finish description + description.setDescription(!definition.desc().isEmpty() ? definition.desc() : null); + description.setHelp(!definition.help().isEmpty() ? definition.help() : null); + description.overrideUsage(!definition.usage().isEmpty() ? definition.usage() : null); + + for (InvokeListener listener : builder.getInvokeListeners()) { + listener.updateDescription(object, method, parameters, description); + } + + // Set parameters + description.setParameters(userParameters); + } + + @Override + public void call(CommandContext context) throws CommandException { + Object[] args = new Object[parameters.length]; + ContextArgumentStack arguments = new ContextArgumentStack(context); + ParameterData parameter = null; + + try { + // preProcess handlers + List handlers = new ArrayList(); + for (InvokeListener listener : builder.getInvokeListeners()) { + InvokeHandler handler = listener.createInvokeHandler(); + handlers.add(handler); + handler.preProcess(object, method, parameters, context); + } + + // Collect parameters + for (int i = 0; i < parameters.length; i++) { + parameter = parameters[i]; + + if (mayConsumeArguments(i, arguments)) { + // Parse the user input into a method argument + ArgumentStack usedArguments = getScopedContext(parameter, arguments); + + try { + args[i] = parameter.getBinding().bind(parameter, usedArguments, false); + } catch (MissingParameterException e) { + // Not optional? Then we can't execute this command + if (!parameter.isOptional()) { + throw e; + } + + args[i] = getDefaultValue(i, arguments); + } + } else { + args[i] = getDefaultValue(i, arguments); + } + } + + // Check for unused arguments + checkUnconsumed(arguments); + + // preInvoke handlers + for (InvokeHandler handler : handlers) { + handler.preInvoke(object, method, parameters, args, context); + } + + // Execute! + method.invoke(object, args); + + // postInvoke handlers + for (InvokeHandler handler : handlers) { + handler.postInvoke(handler, method, parameters, args, context); + } + } catch (MissingParameterException e) { + throw new InvalidUsageException( + "Too few parameters!", getDescription()); + } catch (UnconsumedParameterException e) { + throw new InvalidUsageException( + "Too many parameters! Unused parameters: " + + e.getUnconsumed(), getDescription()); + } catch (ParameterException e) { + if (e.getCause() != null) { + for (ExceptionConverter converter : builder.getExceptionConverters()) { + converter.convert(e.getCause()); + } + } + + String name = parameter.getName(); + + throw new InvalidUsageException("For parameter '" + name + "': " + + e.getMessage(), getDescription()); + } catch (InvocationTargetException e) { + for (ExceptionConverter converter : builder.getExceptionConverters()) { + converter.convert(e.getCause()); + } + throw new WrappedCommandException(e); + } catch (IllegalArgumentException e) { + throw new WrappedCommandException(e); + } catch (CommandException e) { + throw e; + } catch (Throwable e) { + throw new WrappedCommandException(e); + } + } + + @Override + public List getSuggestions(CommandContext context) throws CommandException { + ContextArgumentStack scoped = new ContextArgumentStack(context); + SuggestionContext suggestable = context.getSuggestionContext(); + + // For /command -f | + // For /command -f flag| + if (suggestable.forFlag()) { + for (int i = 0; i < parameters.length; i++) { + ParameterData parameter = parameters[i]; + + if (parameter.getFlag() == suggestable.getFlag()) { + String prefix = context.getFlag(parameter.getFlag()); + if (prefix == null) { + prefix = ""; + } + + return parameter.getBinding().getSuggestions(parameter, prefix); + } + } + + // This should not happen + return new ArrayList(); + } + + int consumerIndex = 0; + ParameterData lastConsumer = null; + String lastConsumed = null; + + for (int i = 0; i < parameters.length; i++) { + ParameterData parameter = parameters[i]; + + if (parameter.getFlag() != null) { + continue; // We already handled flags + } + + try { + scoped.mark(); + parameter.getBinding().bind(parameter, scoped, true); + if (scoped.wasConsumed()) { + lastConsumer = parameter; + lastConsumed = scoped.getConsumed(); + consumerIndex++; + } + } catch (MissingParameterException e) { + // For /command value1 |value2 + // For /command |value1 value2 + if (suggestable.forHangingValue()) { + return parameter.getBinding().getSuggestions(parameter, ""); + } else { + // For /command value1| value2 + if (lastConsumer != null) { + return lastConsumer.getBinding() + .getSuggestions(lastConsumer, lastConsumed); + // For /command| value1 value2 + // This should never occur + } else { + throw new RuntimeException("Invalid suggestion context"); + } + } + } catch (ParameterException e) { + if (suggestable.forHangingValue()) { + String name = getDescription().getParameters() + .get(consumerIndex).getName(); + + throw new InvalidUsageException("For parameter '" + name + "': " + + e.getMessage(), getDescription()); + } else { + return parameter.getBinding().getSuggestions(parameter, ""); + } + } + } + + // For /command value1 value2 | + if (suggestable.forHangingValue()) { + // There's nothing that we can suggest because there's no more parameters + // to add on, and we can't change the previous parameter + return new ArrayList(); + } else { + // For /command value1 value2| + if (lastConsumer != null) { + return lastConsumer.getBinding() + .getSuggestions(lastConsumer, lastConsumed); + // This should never occur + } else { + throw new RuntimeException("Invalid suggestion context"); + } + + } + } + + @Override + public Set getValueFlags() { + return valueFlags; + } + + @Override + public SimpleDescription getDescription() { + return description; + } + + /** + * Get the right {@link ArgumentStack}. + * + * @param parameter the parameter + * @param existing the existing scoped context + * @return the context to use + */ + private static ArgumentStack getScopedContext(Parameter parameter, ArgumentStack existing) { + if (parameter.getFlag() != null) { + CommandContext context = existing.getContext(); + + if (parameter.isValueFlag()) { + return new StringArgumentStack( + context, context.getFlag(parameter.getFlag()), false); + } else { + String v = context.hasFlag(parameter.getFlag()) ? "true" : "false"; + return new StringArgumentStack(context, v, true); + } + } + + return existing; + } + + /** + * Get whether a parameter is allowed to consume arguments. + * + * @param i the index of the parameter + * @param scoped the scoped context + * @return true if arguments may be consumed + */ + private boolean mayConsumeArguments(int i, ContextArgumentStack scoped) { + CommandContext context = scoped.getContext(); + ParameterData parameter = parameters[i]; + + // Flag parameters: Always consume + // Required non-flag parameters: Always consume + // Optional non-flag parameters: + // - Before required parameters: Consume if there are 'left over' args + // - At the end: Always consumes + + if (parameter.isOptional() && parameter.getFlag() == null) { + int numberFree = context.argsLength() - scoped.position(); + for (int j = i; j < parameters.length; j++) { + if (parameters[j].isNonFlagConsumer() && !parameters[j].isOptional()) { + // We already checked if the consumed count was > -1 + // when we created this object + numberFree -= parameters[j].getConsumedCount(); + } + } + + // Skip this optional parameter + if (numberFree < 1) { + return false; + } + } + + return true; + } + + /** + * Get the default value for a parameter. + * + * @param i the index of the parameter + * @param scoped the scoped context + * @return a value + * @throws ParameterException on an error + * @throws CommandException on an error + */ + private Object getDefaultValue(int i, ContextArgumentStack scoped) + throws ParameterException, CommandException { + CommandContext context = scoped.getContext(); + ParameterData parameter = parameters[i]; + + String[] defaultValue = parameter.getDefaultValue(); + if (defaultValue != null) { + try { + return parameter.getBinding().bind( + parameter, new StringArgumentStack( + context, defaultValue, false), false); + } catch (MissingParameterException e) { + throw new ParametricException( + "The default value of the parameter using the binding " + + parameter.getBinding().getClass() + " in the method\n" + + method.toGenericString() + "\nis invalid"); + } + } + + return null; + } + + + /** + * Check to see if all arguments, including flag arguments, were consumed. + * + * @param scoped the argument scope + * @throws UnconsumedParameterException thrown if parameters were not consumed + */ + private void checkUnconsumed(ContextArgumentStack scoped) + throws UnconsumedParameterException { + CommandContext context = scoped.getContext(); + String unconsumed; + String unconsumedFlags = getUnusedFlags(context); + + if ((unconsumed = scoped.getUnconsumed()) != null) { + throw new UnconsumedParameterException(unconsumed + " " + unconsumedFlags); + } + + if (unconsumedFlags != null) { + throw new UnconsumedParameterException(unconsumedFlags); + } + } + + /** + * Get any unused flag arguments. + * + * @param context the command context + * @param parameters the list of parameters + */ + private String getUnusedFlags(CommandContext context) { + Set unusedFlags = null; + for (char flag : context.getFlags()) { + boolean found = false; + for (int i = 0; i < parameters.length; i++) { + Character paramFlag = parameters[i].getFlag(); + if (paramFlag != null && flag == paramFlag) { + found = true; + break; + } + } + + if (!found) { + if (unusedFlags == null) { + unusedFlags = new HashSet(); + } + unusedFlags.add(flag); + } + } + + if (unusedFlags != null) { + StringBuilder builder = new StringBuilder(); + for (Character flag : unusedFlags) { + builder.append("-").append(flag).append(" "); + } + + return builder.toString().trim(); + } + + return null; + } + + /** + * Generate a name for a parameter. + * + * @param type the type + * @param classifier the classifier + * @param index the index + * @return a generated name + */ + private static String generateName(Type type, Annotation classifier, int index) { + if (classifier != null) { + return classifier.annotationType().getSimpleName().toLowerCase(); + } else { + if (type instanceof Class) { + return ((Class) type).getSimpleName().toLowerCase(); + } else { + return "unknown" + index; + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/sk89q/rebar/command/parametric/ParametricException.java b/src/main/java/com/sk89q/rebar/command/parametric/ParametricException.java new file mode 100644 index 000000000..24176200c --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/ParametricException.java @@ -0,0 +1,27 @@ +package com.sk89q.rebar.command.parametric; + +/** + * Thrown if the {@link ParametricBuilder} can't build commands from + * an object for whatever reason. + */ +public class ParametricException extends RuntimeException { + + private static final long serialVersionUID = -5426219576099680971L; + + public ParametricException() { + super(); + } + + public ParametricException(String message, Throwable cause) { + super(message, cause); + } + + public ParametricException(String message) { + super(message); + } + + public ParametricException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/PermissionsHandler.java b/src/main/java/com/sk89q/rebar/command/parametric/PermissionsHandler.java new file mode 100644 index 000000000..585799bf3 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/PermissionsHandler.java @@ -0,0 +1,66 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import java.lang.reflect.Method; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.CommandPermissions; +import com.sk89q.minecraft.util.commands.CommandPermissionsException; + +/** + * A handler for the {@link CommandPermissions} annotation. + */ +public abstract class PermissionsHandler extends AbstractInvokeListener implements InvokeHandler { + + @Override + public InvokeHandler createInvokeHandler() { + return this; + } + + @Override + public void preProcess(Object object, Method method, + ParameterData[] parameters, CommandContext context) + throws CommandException, ParameterException { + CommandPermissions annotation = method.getAnnotation(CommandPermissions.class); + if (annotation != null) { + for (String perm : annotation.value()) { + if (hasPermission(context, perm)) { + return; + } + } + + throw new CommandPermissionsException(); + } + } + + @Override + public void preInvoke(Object object, Method method, ParameterData[] parameters, + Object[] args, CommandContext context) throws CommandException { + } + + @Override + public void postInvoke(Object object, Method method, ParameterData[] parameters, + Object[] args, CommandContext context) throws CommandException { + } + + protected abstract boolean hasPermission(CommandContext context, String permission); + +} diff --git a/src/main/java/com/sk89q/rebar/command/parametric/StringArgumentStack.java b/src/main/java/com/sk89q/rebar/command/parametric/StringArgumentStack.java new file mode 100644 index 000000000..b81526790 --- /dev/null +++ b/src/main/java/com/sk89q/rebar/command/parametric/StringArgumentStack.java @@ -0,0 +1,127 @@ +// $Id$ +/* + * This file is a part of WorldEdit. + * Copyright (c) sk89q + * Copyright (c) the WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . +*/ + +package com.sk89q.rebar.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.rebar.command.MissingParameterException; +import com.sk89q.util.StringUtil; + +/** + * A virtual scope that does not actually read from the underlying + * {@link CommandContext}. + */ +public class StringArgumentStack implements ArgumentStack { + + private final boolean nonNullBoolean; + private final CommandContext context; + private final String[] arguments; + private int index = 0; + + /** + * Create a new instance using the given context. + * + * @param context the context + * @param arguments a list of arguments + * @param nonNullBoolean true to have {@link #nextBoolean()} return false instead of null + */ + public StringArgumentStack( + CommandContext context, String[] arguments, boolean nonNullBoolean) { + this.context = context; + this.arguments = arguments; + this.nonNullBoolean = nonNullBoolean; + } + + /** + * Create a new instance using the given context. + * + * @param context the context + * @param arguments an argument string to be parsed + * @param nonNullBoolean true to have {@link #nextBoolean()} return false instead of null + */ + public StringArgumentStack( + CommandContext context, String arguments, boolean nonNullBoolean) { + this.context = context; + this.arguments = CommandContext.split(arguments); + this.nonNullBoolean = nonNullBoolean; + } + + @Override + public String next() throws ParameterException { + try { + return arguments[index++]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new MissingParameterException(); + } + } + + @Override + public Integer nextInt() throws ParameterException { + try { + return Integer.parseInt(next()); + } catch (NumberFormatException e) { + throw new ParameterException( + "Expected a number, got '" + context.getString(index - 1) + "'"); + } + } + + @Override + public Double nextDouble() throws ParameterException { + try { + return Double.parseDouble(next()); + } catch (NumberFormatException e) { + throw new ParameterException( + "Expected a number, got '" + context.getString(index - 1) + "'"); + } + } + + @Override + public Boolean nextBoolean() throws ParameterException { + try { + return next().equalsIgnoreCase("true"); + } catch (IndexOutOfBoundsException e) { + if (nonNullBoolean) { // Special case + return false; + } + + throw new MissingParameterException(); + } + } + + @Override + public String remaining() throws ParameterException { + try { + String value = StringUtil.joinString(arguments, " ", index); + markConsumed(); + return value; + } catch (IndexOutOfBoundsException e) { + throw new MissingParameterException(); + } + } + + @Override + public void markConsumed() { + index = arguments.length; + } + + @Override + public CommandContext getContext() { + return context; + } + +}