diff --git a/build.gradle b/build.gradle index ea1ad00fa..118600546 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ dependencies { compile group: 'com.google.guava', name: 'guava', version:'10.0.1' compile group: 'com.sk89q', name: 'jchronic', version:'0.2.4a' compile group: 'com.google.code.findbugs', name: 'jsr305', version: '1.3.9' + compile group: 'com.thoughtworks.paranamer', name: 'paranamer', version: '2.6' testCompile group: 'org.mockito', name: 'mockito-core', version:'1.9.0-rc1' } @@ -87,6 +88,7 @@ shadow { destinationDir "${buildDir}/libs/" artifactSet { include '*:jchronic:jar:' + include '*:paranamer:jar:' } } diff --git a/pom.xml b/pom.xml index b6b154888..b9d717b8b 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,13 @@ + + + sk89q-repo + http://maven.sk89q.com/repo/ + + + @@ -151,6 +158,15 @@ jar + + + com.thoughtworks.paranamer + paranamer + 2.6 + compile + jar + + org.mockito @@ -297,6 +313,26 @@ + + + com.thoughtworks.paranamer + paranamer-maven-plugin-largestack + 2.5.5-SNAPSHOT + + + run + compile + + ${project.build.sourceDirectory} + ${project.build.outputDirectory} + + + generate + + + + + org.apache.maven.plugins @@ -352,6 +388,7 @@ com.sk89q:jchronic + com.thoughtworks.paranamer:paranamer diff --git a/src/bukkit/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java b/src/bukkit/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java index 03f4510d3..d86affe9d 100644 --- a/src/bukkit/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java +++ b/src/bukkit/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java @@ -21,13 +21,16 @@ package com.sk89q.worldedit.bukkit; import com.sk89q.bukkit.util.CommandInfo; import com.sk89q.bukkit.util.CommandRegistration; -import com.sk89q.minecraft.util.commands.Command; -import com.sk89q.minecraft.util.commands.CommandPermissions; -import com.sk89q.minecraft.util.commands.CommandsManager; -import com.sk89q.worldedit.*; +import com.sk89q.worldedit.BiomeTypes; +import com.sk89q.worldedit.LocalConfiguration; +import com.sk89q.worldedit.LocalWorld; +import com.sk89q.worldedit.ServerInterface; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Preference; +import com.sk89q.worldedit.util.command.CommandMapping; +import com.sk89q.worldedit.util.command.Description; +import com.sk89q.worldedit.util.command.Dispatcher; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Server; @@ -35,8 +38,10 @@ import org.bukkit.World; import org.bukkit.entity.EntityType; import javax.annotation.Nullable; -import java.lang.reflect.Method; -import java.util.*; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; public class BukkitServerInterface extends ServerInterface { public Server server; @@ -118,25 +123,17 @@ public class BukkitServerInterface extends ServerInterface { } @Override - public void onCommandRegistration(List commands, CommandsManager manager) { + public void registerCommands(Dispatcher dispatcher) { List toRegister = new ArrayList(); - for (Command command : commands) { - List permissions = null; - Method cmdMethod = manager.getMethods().get(null).get(command.aliases()[0]); - Map childMethods = manager.getMethods().get(cmdMethod); + for (CommandMapping command : dispatcher.getCommands()) { + Description description = command.getDescription(); + List permissions = description.getPermissions(); + String[] permissionsArray = new String[permissions.size()]; + permissions.toArray(permissionsArray); - if (cmdMethod != null && cmdMethod.isAnnotationPresent(CommandPermissions.class)) { - permissions = Arrays.asList(cmdMethod.getAnnotation(CommandPermissions.class).value()); - } else if (cmdMethod != null && childMethods != null && childMethods.size() > 0) { - permissions = new ArrayList(); - for (Method m : childMethods.values()) { - if (m.isAnnotationPresent(CommandPermissions.class)) { - permissions.addAll(Arrays.asList(m.getAnnotation(CommandPermissions.class).value())); - } - } - } - - toRegister.add(new CommandInfo(command.usage(), command.desc(), command.aliases(), commands, permissions == null ? null : permissions.toArray(new String[permissions.size()]))); + toRegister.add(new CommandInfo( + description.getUsage(), description.getDescription(), + command.getAllAliases(), dispatcher, permissionsArray)); } dynamicCommands.register(toRegister); diff --git a/src/forge/java/com/sk89q/worldedit/forge/ForgePlatform.java b/src/forge/java/com/sk89q/worldedit/forge/ForgePlatform.java index d983c5885..818a14573 100644 --- a/src/forge/java/com/sk89q/worldedit/forge/ForgePlatform.java +++ b/src/forge/java/com/sk89q/worldedit/forge/ForgePlatform.java @@ -19,13 +19,15 @@ package com.sk89q.worldedit.forge; -import com.sk89q.minecraft.util.commands.Command; import com.sk89q.worldedit.BiomeTypes; import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.ServerInterface; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Preference; +import com.sk89q.worldedit.util.command.CommandMapping; +import com.sk89q.worldedit.util.command.Description; +import com.sk89q.worldedit.util.command.Dispatcher; import com.sk89q.worldedit.world.World; import cpw.mods.fml.common.FMLCommonHandler; import net.minecraft.command.CommandBase; @@ -133,19 +135,22 @@ class ForgePlatform extends ServerInterface { } @Override - public void onCommandRegistration(List commands) { + public void registerCommands(Dispatcher dispatcher) { if (server == null) return; ServerCommandManager mcMan = (ServerCommandManager) server.getCommandManager(); - for (final Command cmd : commands) { + + for (final CommandMapping command : dispatcher.getCommands()) { + final Description description = command.getDescription(); + mcMan.registerCommand(new CommandBase() { @Override public String getCommandName() { - return cmd.aliases()[0]; + return command.getPrimaryAlias(); } @Override public List getCommandAliases() { - return Arrays.asList(cmd.aliases()); + return Arrays.asList(command.getAllAliases()); } @Override @@ -153,12 +158,14 @@ class ForgePlatform extends ServerInterface { @Override public String getCommandUsage(ICommandSender icommandsender) { - return "/" + cmd.aliases()[0] + " " + cmd.usage(); + return "/" + command.getPrimaryAlias() + " " + description.getUsage(); } @Override - public int compareTo(Object o) { - if (o instanceof ICommand) { + public int compareTo(@Nullable Object o) { + if (o == null) { + return -1; + } else if (o instanceof ICommand) { return super.compareTo((ICommand) o); } else { return -1; diff --git a/src/main/build/import-control.xml b/src/main/build/import-control.xml index 56ba42a5c..9ef48920d 100644 --- a/src/main/build/import-control.xml +++ b/src/main/build/import-control.xml @@ -9,6 +9,7 @@ + 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 3e26fdd0b..31900a9d2 100644 --- a/src/main/java/com/sk89q/minecraft/util/commands/CommandContext.java +++ b/src/main/java/com/sk89q/minecraft/util/commands/CommandContext.java @@ -28,15 +28,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 { @@ -44,28 +51,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; } @@ -113,9 +142,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; @@ -140,16 +174,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() { @@ -176,6 +224,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)); @@ -271,4 +331,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 c87f0391c..eae94c75f 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..e0053f0b3 --- /dev/null +++ b/src/main/java/com/sk89q/minecraft/util/commands/CommandLocals.java @@ -0,0 +1,50 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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..7f435cf13 --- /dev/null +++ b/src/main/java/com/sk89q/minecraft/util/commands/SuggestionContext.java @@ -0,0 +1,68 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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; + +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/worldedit/EditSessionFactory.java b/src/main/java/com/sk89q/worldedit/EditSessionFactory.java index e16da791f..285ecc4ab 100644 --- a/src/main/java/com/sk89q/worldedit/EditSessionFactory.java +++ b/src/main/java/com/sk89q/worldedit/EditSessionFactory.java @@ -19,6 +19,7 @@ package com.sk89q.worldedit; +import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.event.extent.EditSessionEvent; import com.sk89q.worldedit.extent.inventory.BlockBag; import com.sk89q.worldedit.util.eventbus.EventBus; @@ -63,7 +64,7 @@ public class EditSessionFactory { * @param maxBlocks the maximum number of blocks that can be changed, or -1 to use no limit * @param player the player that the {@link EditSession} is for */ - public EditSession getEditSession(World world, int maxBlocks, LocalPlayer player) { + public EditSession getEditSession(World world, int maxBlocks, Player player) { throw new IllegalArgumentException("This class is being removed"); } @@ -96,7 +97,7 @@ public class EditSessionFactory { * @param blockBag an optional {@link BlockBag} to use, otherwise null * @param player the player that the {@link EditSession} is for */ - public EditSession getEditSession(World world, int maxBlocks, BlockBag blockBag, LocalPlayer player) { + public EditSession getEditSession(World world, int maxBlocks, BlockBag blockBag, Player player) { throw new IllegalArgumentException("This class is being removed"); } @@ -123,7 +124,7 @@ public class EditSessionFactory { } @Override - public EditSession getEditSession(World world, int maxBlocks, LocalPlayer player) { + public EditSession getEditSession(World world, int maxBlocks, Player player) { return new EditSession(eventBus, world, maxBlocks, null, new EditSessionEvent(world, player, maxBlocks, null)); } @@ -133,7 +134,7 @@ public class EditSessionFactory { } @Override - public EditSession getEditSession(World world, int maxBlocks, BlockBag blockBag, LocalPlayer player) { + public EditSession getEditSession(World world, int maxBlocks, BlockBag blockBag, Player player) { return new EditSession(eventBus, world, maxBlocks, blockBag, new EditSessionEvent(world, player, maxBlocks, null)); } diff --git a/src/main/java/com/sk89q/worldedit/LocalSession.java b/src/main/java/com/sk89q/worldedit/LocalSession.java index 5b07345b5..935980eb9 100644 --- a/src/main/java/com/sk89q/worldedit/LocalSession.java +++ b/src/main/java/com/sk89q/worldedit/LocalSession.java @@ -27,6 +27,7 @@ import com.sk89q.worldedit.command.tool.BlockTool; import com.sk89q.worldedit.command.tool.BrushTool; import com.sk89q.worldedit.command.tool.SinglePickaxe; import com.sk89q.worldedit.command.tool.Tool; +import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extent.inventory.BlockBag; import com.sk89q.worldedit.internal.cui.CUIEvent; @@ -419,7 +420,7 @@ public class LocalSession { * @param player * @return */ - public BlockBag getBlockBag(LocalPlayer player) { + public BlockBag getBlockBag(Player player) { if (!useInventory) { return null; } @@ -550,7 +551,7 @@ public class LocalSession { * * @param player */ - public void tellVersion(LocalPlayer player) { + public void tellVersion(Actor player) { if (config.showFirstUseVersion) { if (!beenToldVersion) { player.printRaw("\u00A78WorldEdit ver. " + WorldEdit.getVersion() @@ -713,6 +714,17 @@ public class LocalSession { * @return */ public EditSession createEditSession(LocalPlayer player) { + return createEditSession((Player) player); + } + + /** + * Construct a new edit session. + * + * @param player + * @return + */ + @SuppressWarnings("deprecation") + public EditSession createEditSession(Player player) { BlockBag blockBag = getBlockBag(player); // Create an edit session @@ -721,8 +733,8 @@ public class LocalSession { getBlockChangeLimit(), blockBag, player); editSession.setFastMode(fastMode); Request.request().setEditSession(editSession); - if (mask != null) { - mask.prepare(this, player, null); + if (mask != null && player instanceof LocalPlayer) { + mask.prepare(this, (LocalPlayer) player, null); } editSession.setMask(mask); diff --git a/src/main/java/com/sk89q/worldedit/WorldEdit.java b/src/main/java/com/sk89q/worldedit/WorldEdit.java index 6dca280b5..969bdf62d 100644 --- a/src/main/java/com/sk89q/worldedit/WorldEdit.java +++ b/src/main/java/com/sk89q/worldedit/WorldEdit.java @@ -19,12 +19,12 @@ package com.sk89q.worldedit; -import com.sk89q.minecraft.util.commands.CommandsManager; import com.sk89q.worldedit.CuboidClipboard.FlipDirection; import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.blocks.BlockType; -import com.sk89q.worldedit.event.platform.BlockInteractEvent; +import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.event.extent.EditSessionEvent; +import com.sk89q.worldedit.event.platform.BlockInteractEvent; import com.sk89q.worldedit.event.platform.CommandEvent; import com.sk89q.worldedit.event.platform.InputType; import com.sk89q.worldedit.event.platform.PlayerInputEvent; @@ -483,7 +483,7 @@ public class WorldEdit { * @return a direction vector * @throws UnknownDirectionException thrown if the direction is not known */ - public Vector getDirection(LocalPlayer player, String dirStr) throws UnknownDirectionException { + public Vector getDirection(Player player, String dirStr) throws UnknownDirectionException { dirStr = dirStr.toLowerCase(); final PlayerDirection dir = getPlayerDirection(player, dirStr); @@ -511,7 +511,7 @@ public class WorldEdit { * @return a direction enum value * @throws UnknownDirectionException thrown if the direction is not known */ - private PlayerDirection getPlayerDirection(LocalPlayer player, String dirStr) throws UnknownDirectionException { + private PlayerDirection getPlayerDirection(Player player, String dirStr) throws UnknownDirectionException { final PlayerDirection dir; switch (dirStr.charAt(0)) { @@ -661,24 +661,6 @@ public class WorldEdit { } } - /** - * Get the map of commands (internal usage only). - * - * @return the commands - */ - public Map getCommands() { - return getCommandsManager().getCommands(); - } - - /** - * Get the commands manager (internal usage only). - * - * @return the commands - */ - public CommandsManager getCommandsManager() { - return getPlatformManager().getCommandManager().getCommands(); - } - /** * Handle a disconnection. * diff --git a/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java b/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java index eec97d650..910064278 100644 --- a/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java +++ b/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java @@ -260,13 +260,6 @@ public class ClipboardCommands { player.printError("This command is no longer used. See //schematic save."); } - @Command( - aliases = { "/schematic", "/schem"}, - desc = "Schematic-related commands" - ) - @NestedCommand(SchematicCommands.class) - public void schematic() {} - @Command( aliases = { "clearclipboard" }, usage = "", diff --git a/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java b/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java index 285901292..16e20ab35 100644 --- a/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java +++ b/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java @@ -23,13 +23,7 @@ import com.sk89q.minecraft.util.commands.Command; import com.sk89q.minecraft.util.commands.CommandContext; import com.sk89q.minecraft.util.commands.CommandPermissions; import com.sk89q.minecraft.util.commands.Console; -import com.sk89q.minecraft.util.commands.NestedCommand; -import com.sk89q.worldedit.EditSession; -import com.sk89q.worldedit.LocalConfiguration; -import com.sk89q.worldedit.LocalPlayer; -import com.sk89q.worldedit.LocalSession; -import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.*; import com.sk89q.worldedit.blocks.ItemType; import com.sk89q.worldedit.masks.Mask; @@ -226,13 +220,4 @@ public class GeneralCommands { } } - @Command( - aliases = { "we", "worldedit" }, - desc = "WorldEdit commands" - ) - @NestedCommand(WorldEditCommands.class) - @Console - public void we(CommandContext args, LocalSession session, LocalPlayer player, - EditSession editSession) throws WorldEditException { - } } diff --git a/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java b/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java index 661b370c9..b0f978438 100644 --- a/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java +++ b/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java @@ -54,15 +54,6 @@ public class SnapshotUtilCommands { this.we = we; } - @Command( - aliases = { "snapshot", "snap" }, - desc = "Snapshot commands" - ) - @NestedCommand(SnapshotCommands.class) - public void snapshot(CommandContext args, LocalSession session, LocalPlayer player, - EditSession editSession) throws WorldEditException { - } - @Command( aliases = { "restore", "/restore" }, usage = "[snapshot]", diff --git a/src/main/java/com/sk89q/worldedit/command/ToolCommands.java b/src/main/java/com/sk89q/worldedit/command/ToolCommands.java index c01084961..4279f0ce6 100644 --- a/src/main/java/com/sk89q/worldedit/command/ToolCommands.java +++ b/src/main/java/com/sk89q/worldedit/command/ToolCommands.java @@ -151,15 +151,6 @@ public class ToolCommands { + ItemType.toHeldName(player.getItemInHand()) + "."); } - @Command( - aliases = { "brush", "br" }, - desc = "Brush tool" - ) - @NestedCommand(BrushCommands.class) - public void brush(CommandContext args, LocalSession session, LocalPlayer player, - EditSession editSession) throws WorldEditException { - } - @Command( aliases = { "deltree" }, usage = "", diff --git a/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java b/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java index 17355a733..a1a537b46 100644 --- a/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java +++ b/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java @@ -22,7 +22,6 @@ package com.sk89q.worldedit.command; import com.sk89q.minecraft.util.commands.Command; import com.sk89q.minecraft.util.commands.CommandContext; import com.sk89q.minecraft.util.commands.CommandPermissions; -import com.sk89q.minecraft.util.commands.NestedCommand; import com.sk89q.worldedit.*; import com.sk89q.worldedit.masks.Mask; import com.sk89q.worldedit.patterns.Pattern; @@ -70,24 +69,6 @@ public class ToolUtilCommands { } - @Command( - aliases = { "superpickaxe", "pickaxe", "sp" }, - desc = "Select super pickaxe mode" - ) - @NestedCommand(SuperPickaxeCommands.class) - public void pickaxe(CommandContext args, LocalSession session, LocalPlayer player, - EditSession editSession) throws WorldEditException { - } - - @Command( - aliases = {"tool"}, - desc = "Select a tool to bind" - ) - @NestedCommand(ToolCommands.class) - public void tool(CommandContext args, LocalSession session, LocalPlayer player, - EditSession editSession) throws WorldEditException { - } - @Command( aliases = { "mask" }, usage = "[mask]", diff --git a/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java b/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java index c8128d1eb..8e57def7c 100644 --- a/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java +++ b/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java @@ -27,6 +27,9 @@ import com.sk89q.worldedit.patterns.Pattern; import com.sk89q.worldedit.patterns.SingleBlockPattern; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.util.command.CommandMapping; +import com.sk89q.worldedit.util.command.Description; +import com.sk89q.worldedit.util.command.Dispatcher; import com.sk89q.worldedit.world.World; import java.util.Comparator; @@ -512,7 +515,7 @@ public class UtilityCommands { } public static void help(CommandContext args, WorldEdit we, LocalSession session, LocalPlayer player, EditSession editSession) { - final CommandsManager commandsManager = we.getCommandsManager(); + final Dispatcher dispatcher = we.getPlatformManager().getCommandManager().getDispatcher(); if (args.argsLength() == 0) { SortedSet commands = new TreeSet(new Comparator() { @@ -525,7 +528,7 @@ public class UtilityCommands { return ret; } }); - commands.addAll(commandsManager.getCommands().keySet()); + commands.addAll(dispatcher.getPrimaryAliases()); StringBuilder sb = new StringBuilder(); boolean first = true; @@ -544,14 +547,26 @@ public class UtilityCommands { return; } - String command = args.getJoinedStrings(0).toLowerCase().replaceAll("/", ""); + String command = args.getJoinedStrings(0).toLowerCase().replaceAll("^/", ""); + CommandMapping mapping = dispatcher.get(command); - String helpMessage = commandsManager.getHelpMessages().get(command); - if (helpMessage == null) { + if (mapping == null) { player.printError("Unknown command '" + command + "'."); return; } - player.print(helpMessage); + Description description = mapping.getDescription(); + + if (description.getUsage() != null) { + player.printDebug("Usage: " + description.getUsage()); + } + + if (description.getHelp() != null) { + player.print(description.getHelp()); + } else if (description.getDescription() != null) { + player.print(description.getDescription()); + } else { + player.print("No further help is available."); + } } } diff --git a/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java b/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java index 4f84ff8bd..a6534f8a4 100644 --- a/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java +++ b/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java @@ -42,15 +42,4 @@ public abstract class AbstractPlatform implements Platform { return Collections.emptyList(); } - @Override - @Deprecated - public void onCommandRegistration(List commands) { - // Do nothing :) - } - - @Override - public void onCommandRegistration(List commands, CommandsManager manager) { - onCommandRegistration(commands); - } - } diff --git a/src/main/java/com/sk89q/worldedit/extension/platform/CommandManager.java b/src/main/java/com/sk89q/worldedit/extension/platform/CommandManager.java index 6eb9f8994..318389579 100644 --- a/src/main/java/com/sk89q/worldedit/extension/platform/CommandManager.java +++ b/src/main/java/com/sk89q/worldedit/extension/platform/CommandManager.java @@ -19,22 +19,35 @@ package com.sk89q.worldedit.extension.platform; -import com.sk89q.minecraft.util.commands.*; -import com.sk89q.util.StringUtil; -import com.sk89q.worldedit.*; -import com.sk89q.worldedit.blocks.ItemType; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.CommandLocals; +import com.sk89q.minecraft.util.commands.CommandPermissionsException; +import com.sk89q.minecraft.util.commands.WrappedCommandException; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.LocalConfiguration; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.command.*; import com.sk89q.worldedit.event.platform.CommandEvent; import com.sk89q.worldedit.session.request.Request; -import com.sk89q.worldedit.util.logging.LogFormat; +import com.sk89q.worldedit.util.CommandLoggingHandler; +import com.sk89q.worldedit.util.CommandPermissionsHandler; +import com.sk89q.worldedit.util.WorldEditBinding; +import com.sk89q.worldedit.util.WorldEditExceptionConverter; +import com.sk89q.worldedit.util.command.Dispatcher; +import com.sk89q.worldedit.util.command.InvalidUsageException; +import com.sk89q.worldedit.util.command.fluent.CommandGraph; +import com.sk89q.worldedit.util.command.parametric.LegacyCommandsHandler; +import com.sk89q.worldedit.util.command.parametric.ParametricBuilder; import com.sk89q.worldedit.util.eventbus.Subscribe; import com.sk89q.worldedit.util.logging.DynamicStreamHandler; +import com.sk89q.worldedit.util.logging.LogFormat; import java.io.File; import java.io.IOException; -import java.lang.reflect.Method; -import java.util.logging.*; -import java.util.regex.Matcher; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.Logger; import static com.google.common.base.Preconditions.checkNotNull; @@ -49,7 +62,7 @@ public final class CommandManager { private static final java.util.regex.Pattern numberFormatExceptionPattern = java.util.regex.Pattern.compile("^For input string: \"(.*)\"$"); private final WorldEdit worldEdit; - private final CommandsManager commands; + private final Dispatcher dispatcher; private final DynamicStreamHandler dynamicHandler = new DynamicStreamHandler(); /** @@ -69,8 +82,56 @@ public final class CommandManager { dynamicHandler.setFormatter(new LogFormat()); // Set up the commands manager - commands = new CommandsManagerImpl(); - commands.setInjector(new SimpleInjector(worldEdit)); + ParametricBuilder builder = new ParametricBuilder(); + builder.addBinding(new WorldEditBinding(worldEdit)); + builder.attach(new CommandPermissionsHandler()); + builder.attach(new WorldEditExceptionConverter(worldEdit)); + builder.attach(new LegacyCommandsHandler()); + builder.attach(new CommandLoggingHandler(worldEdit, logger)); + + dispatcher = new CommandGraph() + .builder(builder) + .commands() + .build(new BiomeCommands(worldEdit)) + .build(new ChunkCommands(worldEdit)) + .build(new ClipboardCommands(worldEdit)) + .build(new GeneralCommands(worldEdit)) + .build(new GenerationCommands(worldEdit)) + .build(new HistoryCommands(worldEdit)) + .build(new NavigationCommands(worldEdit)) + .build(new RegionCommands(worldEdit)) + .build(new ScriptingCommands(worldEdit)) + .build(new SelectionCommands(worldEdit)) + .build(new SnapshotUtilCommands(worldEdit)) + .build(new ToolUtilCommands(worldEdit)) + .build(new ToolCommands(worldEdit)) + .build(new UtilityCommands(worldEdit)) + .group("worldedit", "we") + .describe("WorldEdit commands") + .build(new WorldEditCommands(worldEdit)) + .parent() + .group("schematic", "schem", "/schematic", "/schem") + .describe("Schematic commands for saving/loading areas") + .build(new SchematicCommands(worldEdit)) + .parent() + .group("snapshot", "snap") + .describe("Schematic commands for saving/loading areas") + .build(new SnapshotCommands(worldEdit)) + .parent() + .group("brush", "br") + .describe("Brushing commands") + .build(new BrushCommands(worldEdit)) + .parent() + .group("superpickaxe", "pickaxe", "sp") + .describe("Super-pickaxe commands") + .build(new SuperPickaxeCommands(worldEdit)) + .parent() + .group("tool") + .describe("Bind functions to held items") + .build(new ToolCommands(worldEdit)) + .parent() + .graph() + .getDispatcher(); } void register(Platform platform) { @@ -95,37 +156,14 @@ public final class CommandManager { } } - register(platform, BiomeCommands.class); - register(platform, ChunkCommands.class); - register(platform, ClipboardCommands.class); - register(platform, GeneralCommands.class); - register(platform, GenerationCommands.class); - register(platform, HistoryCommands.class); - register(platform, NavigationCommands.class); - register(platform, RegionCommands.class); - register(platform, ScriptingCommands.class); - register(platform, SelectionCommands.class); - register(platform, SnapshotUtilCommands.class); - register(platform, ToolUtilCommands.class); - register(platform, ToolCommands.class); - register(platform, UtilityCommands.class); + platform.registerCommands(dispatcher); } void unregister() { dynamicHandler.setHandler(null); } - private void register(Platform platform, Class clazz) { - platform.onCommandRegistration(commands.registerAndReturn(clazz), commands); - } - - public CommandsManager getCommands() { - return commands; - } - public String[] commandDetection(String[] split) { - Request.reset(); - split[0] = split[0].substring(1); // Quick script shortcut @@ -140,14 +178,13 @@ public final class CommandManager { String searchCmd = split[0].toLowerCase(); // Try to detect the command - if (commands.hasCommand(searchCmd)) { - } else if (worldEdit.getConfiguration().noDoubleSlash && commands.hasCommand("/" + searchCmd)) { + if (dispatcher.contains(searchCmd)) { + } else if (worldEdit.getConfiguration().noDoubleSlash && dispatcher.contains("/" + searchCmd)) { split[0] = "/" + split[0]; } else if (split[0].length() >= 2 && split[0].charAt(0) == '/' - && commands.hasCommand(searchCmd.substring(1))) { + && dispatcher.contains(searchCmd.substring(1))) { split[0] = split[0].substring(1); } - return split; } @@ -155,185 +192,69 @@ public final class CommandManager { public void handleCommand(CommandEvent event) { Request.reset(); - LocalPlayer player = event.getPlayer(); - String[] split = event.getArguments(); + Actor actor = event.getPlayer(); + String split[] = commandDetection(event.getArguments()); + + // No command found! + if (!dispatcher.contains(split[0])) { + return; + } + + LocalSession session = worldEdit.getSessionManager().get(actor); + LocalConfiguration config = worldEdit.getConfiguration(); + + CommandLocals locals = new CommandLocals(); + locals.put(Actor.class, actor); + + long start = System.currentTimeMillis(); try { - split = commandDetection(split); + dispatcher.call(split, locals); + } catch (CommandPermissionsException e) { + actor.printError("You don't have permission to do this."); + } catch (InvalidUsageException e) { + actor.printError(e.getMessage() + "\nUsage: " + e.getUsage("/")); + } catch (WrappedCommandException e) { + Throwable t = e.getCause(); + actor.printError("Please report this error: [See console]"); + actor.printRaw(t.getClass().getName() + ": " + t.getMessage()); + t.printStackTrace(); + } catch (CommandException e) { + actor.printError(e.getMessage()); + } finally { + EditSession editSession = locals.get(EditSession.class); - // No command found! - if (!commands.hasCommand(split[0])) { - return; - } - - LocalSession session = worldEdit.getSession(player); - EditSession editSession = session.createEditSession(player); - editSession.enableQueue(); - - session.tellVersion(player); - - long start = System.currentTimeMillis(); - - try { - commands.execute(split, player, session, player, editSession); - } catch (CommandPermissionsException e) { - player.printError("You don't have permission to do this."); - } catch (MissingNestedCommandException e) { - player.printError(e.getUsage()); - } catch (CommandUsageException e) { - player.printError(e.getMessage()); - player.printError(e.getUsage()); - } catch (PlayerNeededException e) { - player.printError(e.getMessage()); - } catch (WrappedCommandException e) { - throw e.getCause(); - } catch (UnhandledCommandException e) { - player.printError("Command could not be handled; invalid sender!"); - event.setCancelled(true); - return; - } finally { + if (editSession != null) { session.remember(editSession); editSession.flushQueue(); - if (worldEdit.getConfiguration().profile) { + if (config.profile) { long time = System.currentTimeMillis() - start; int changed = editSession.getBlockChangeCount(); if (time > 0) { double throughput = changed / (time / 1000.0); - player.printDebug((time / 1000.0) + "s elapsed (history: " + actor.printDebug((time / 1000.0) + "s elapsed (history: " + changed + " changed; " + Math.round(throughput) + " blocks/sec)."); } else { - player.printDebug((time / 1000.0) + "s elapsed."); + actor.printDebug((time / 1000.0) + "s elapsed."); } } - worldEdit.flushBlockBag(player, editSession); + worldEdit.flushBlockBag(event.getPlayer(), editSession); } - } catch (NumberFormatException e) { - final Matcher matcher = numberFormatExceptionPattern.matcher(e.getMessage()); - - if (matcher.matches()) { - player.printError("Number expected; string \"" + matcher.group(1) + "\" given."); - } else { - player.printError("Number expected; string given."); - } - } catch (IncompleteRegionException e) { - player.printError("Make a region selection first."); - } catch (UnknownItemException e) { - player.printError("Block name '" + e.getID() + "' was not recognized."); - } catch (InvalidItemException e) { - player.printError(e.getMessage()); - } catch (DisallowedItemException e) { - player.printError("Block '" + e.getID() + "' not allowed (see WorldEdit configuration)."); - } catch (MaxChangedBlocksException e) { - player.printError("Max blocks changed in an operation reached (" - + e.getBlockLimit() + ")."); - } catch (MaxBrushRadiusException e) { - player.printError("Maximum allowed brush size: " + worldEdit.getConfiguration().maxBrushRadius); - } catch (MaxRadiusException e) { - player.printError("Maximum allowed size: " + worldEdit.getConfiguration().maxRadius); - } catch (UnknownDirectionException e) { - player.printError("Unknown direction: " + e.getDirection()); - } catch (InsufficientArgumentsException e) { - player.printError(e.getMessage()); - } catch (EmptyClipboardException e) { - player.printError("Your clipboard is empty. Use //copy first."); - } catch (InvalidFilenameException e) { - player.printError("Filename '" + e.getFilename() + "' invalid: " - + e.getMessage()); - } catch (FilenameResolutionException e) { - player.printError("File '" + e.getFilename() + "' resolution error: " - + e.getMessage()); - } catch (InvalidToolBindException e) { - player.printError("Can't bind tool to " - + ItemType.toHeldName(e.getItemId()) + ": " + e.getMessage()); - } catch (FileSelectionAbortedException e) { - player.printError("File selection aborted."); - } catch (WorldEditException e) { - player.printError(e.getMessage()); - } catch (Throwable excp) { - player.printError("Please report this error: [See console]"); - player.printRaw(excp.getClass().getName() + ": " + excp.getMessage()); - excp.printStackTrace(); } event.setCancelled(true); } - private class CommandsManagerImpl extends CommandsManager { - @Override - protected void checkPermission(LocalPlayer player, Method method) throws CommandException { - if (!player.isPlayer() && !method.isAnnotationPresent(Console.class)) { - throw new UnhandledCommandException(); - } - - super.checkPermission(player, method); - } - - @Override - public boolean hasPermission(LocalPlayer player, String perm) { - return player.hasPermission(perm); - } - - @Override - public void invokeMethod(Method parent, String[] args, - LocalPlayer player, Method method, Object instance, - Object[] methodArgs, int level) throws CommandException { - if (worldEdit.getConfiguration().logCommands) { - final Logging loggingAnnotation = method.getAnnotation(Logging.class); - - final Logging.LogMode logMode; - if (loggingAnnotation == null) { - logMode = null; - } else { - logMode = loggingAnnotation.value(); - } - - String msg = "WorldEdit: " + player.getName(); - if (player.isPlayer()) { - msg += " (in \"" + player.getWorld().getName() + "\")"; - } - msg += ": " + StringUtil.joinString(args, " "); - if (logMode != null && player.isPlayer()) { - Vector position = player.getPosition(); - final LocalSession session = worldEdit.getSessionManager().get(player); - - switch (logMode) { - case PLACEMENT: - try { - position = session.getPlacementPosition(player); - } catch (IncompleteRegionException e) { - break; - } - /* FALL-THROUGH */ - - case POSITION: - msg += " - Position: " + position; - break; - - case ALL: - msg += " - Position: " + position; - /* FALL-THROUGH */ - - case ORIENTATION_REGION: - msg += " - Orientation: " + player.getCardinalDirection().name(); - /* FALL-THROUGH */ - - case REGION: - try { - msg += " - Region: " + session.getSelection(player.getWorld()); - } catch (IncompleteRegionException e) { - break; - } - break; - } - } - - getLogger().info(msg); - } - super.invokeMethod(parent, args, player, method, instance, methodArgs, level); - } + /** + * Get the command dispatcher instance. + * + * @return the command dispatcher + */ + public Dispatcher getDispatcher() { + return dispatcher; } public static Logger getLogger() { diff --git a/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java b/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java index b4f542599..bc90eaa5d 100644 --- a/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java +++ b/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java @@ -19,12 +19,10 @@ package com.sk89q.worldedit.extension.platform; -import com.sk89q.minecraft.util.commands.Command; -import com.sk89q.minecraft.util.commands.CommandsManager; import com.sk89q.worldedit.BiomeTypes; import com.sk89q.worldedit.LocalConfiguration; -import com.sk89q.worldedit.LocalPlayer; import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.util.command.Dispatcher; import com.sk89q.worldedit.world.World; import javax.annotation.Nullable; @@ -100,10 +98,12 @@ public interface Platform { */ @Nullable World matchWorld(World world); - @Deprecated - void onCommandRegistration(List commands); - - void onCommandRegistration(List commands, CommandsManager manager); + /** + * Register the commands contained within the given command dispatcher. + * + * @param dispatcher the dispatcher + */ + void registerCommands(Dispatcher dispatcher); /** * Register game hooks. diff --git a/src/main/java/com/sk89q/worldedit/internal/ServerInterfaceAdapter.java b/src/main/java/com/sk89q/worldedit/internal/ServerInterfaceAdapter.java index ab582847f..16d86ee5e 100644 --- a/src/main/java/com/sk89q/worldedit/internal/ServerInterfaceAdapter.java +++ b/src/main/java/com/sk89q/worldedit/internal/ServerInterfaceAdapter.java @@ -19,16 +19,14 @@ package com.sk89q.worldedit.internal; -import com.sk89q.minecraft.util.commands.Command; -import com.sk89q.minecraft.util.commands.CommandsManager; import com.sk89q.worldedit.BiomeTypes; import com.sk89q.worldedit.LocalConfiguration; -import com.sk89q.worldedit.LocalPlayer; import com.sk89q.worldedit.ServerInterface; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Platform; import com.sk89q.worldedit.extension.platform.Preference; +import com.sk89q.worldedit.util.command.Dispatcher; import com.sk89q.worldedit.world.World; import javax.annotation.Nullable; @@ -97,14 +95,8 @@ public class ServerInterfaceAdapter extends ServerInterface { } @Override - @Deprecated - public void onCommandRegistration(List commands) { - platform.onCommandRegistration(commands); - } - - @Override - public void onCommandRegistration(List commands, CommandsManager manager) { - platform.onCommandRegistration(commands, manager); + public void registerCommands(Dispatcher dispatcher) { + platform.registerCommands(dispatcher); } @Override diff --git a/src/main/java/com/sk89q/worldedit/internal/annotation/Direction.java b/src/main/java/com/sk89q/worldedit/internal/annotation/Direction.java new file mode 100644 index 000000000..45d495bbd --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/internal/annotation/Direction.java @@ -0,0 +1,39 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.internal.annotation; + +import com.sk89q.worldedit.Vector; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a {@link Vector} parameter to inject a direction. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Direction { + + public static final String AIM = "me"; + +} diff --git a/src/main/java/com/sk89q/worldedit/internal/annotation/Selection.java b/src/main/java/com/sk89q/worldedit/internal/annotation/Selection.java new file mode 100644 index 000000000..9647c360f --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/internal/annotation/Selection.java @@ -0,0 +1,34 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.internal.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that this value should come from the current selection. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Selection { + +} diff --git a/src/main/java/com/sk89q/worldedit/util/CommandLoggingHandler.java b/src/main/java/com/sk89q/worldedit/util/CommandLoggingHandler.java new file mode 100644 index 000000000..f75e02b31 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/CommandLoggingHandler.java @@ -0,0 +1,147 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.Logging; +import com.sk89q.worldedit.*; +import com.sk89q.worldedit.util.command.parametric.AbstractInvokeListener; +import com.sk89q.worldedit.util.command.parametric.InvokeHandler; +import com.sk89q.worldedit.util.command.parametric.ParameterData; +import com.sk89q.worldedit.util.command.parametric.ParameterException; + +import java.io.Closeable; +import java.lang.reflect.Method; +import java.util.logging.Handler; +import java.util.logging.Logger; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Logs called commands to a logger. + */ +public class CommandLoggingHandler extends AbstractInvokeListener implements InvokeHandler, Closeable { + + private final WorldEdit worldEdit; + private final Logger logger; + + /** + * Create a new instance. + * + * @param worldEdit an instance of WorldEdit + * @param logger the logger to send messages to + */ + public CommandLoggingHandler(WorldEdit worldEdit, Logger logger) { + checkNotNull(worldEdit); + checkNotNull(logger); + this.worldEdit = worldEdit; + this.logger = logger; + } + + @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 CommandException { + Logging loggingAnnotation = method.getAnnotation(Logging.class); + Logging.LogMode logMode; + StringBuilder builder = new StringBuilder(); + + if (loggingAnnotation == null) { + logMode = null; + } else { + logMode = loggingAnnotation.value(); + } + + LocalPlayer sender = context.getLocals().get(LocalPlayer.class); + if (sender == null) { + return; + } + + builder.append("WorldEdit: ").append(sender.getName()); + if (sender.isPlayer()) { + builder.append(" (in \"" + sender.getWorld().getName() + "\")"); + } + + builder.append(": ").append(context.getCommand()); + + if (context.argsLength() > 0) { + builder.append(" ").append(context.getJoinedStrings(0)); + } + + if (logMode != null && sender.isPlayer()) { + Vector position = sender.getPosition(); + LocalSession session = worldEdit.getSession(sender); + + switch (logMode) { + case PLACEMENT: + try { + position = session.getPlacementPosition(sender); + } catch (IncompleteRegionException e) { + break; + } + /* FALL-THROUGH */ + + case POSITION: + builder.append(" - Position: " + position); + break; + + case ALL: + builder.append(" - Position: " + position); + /* FALL-THROUGH */ + + case ORIENTATION_REGION: + builder.append(" - Orientation: " + + sender.getCardinalDirection().name()); + /* FALL-THROUGH */ + + case REGION: + try { + builder.append(" - Region: ") + .append(session.getSelection(sender.getWorld())); + } catch (IncompleteRegionException e) { + break; + } + break; + } + } + + logger.info(builder.toString()); + } + + @Override + public void postInvoke(Object object, Method method, ParameterData[] parameters, Object[] args, CommandContext context) throws CommandException { + } + + @Override + public InvokeHandler createInvokeHandler() { + return this; + } + + @Override + public void close() { + for (Handler h : logger.getHandlers()) { + h.close(); + } + } + +} diff --git a/src/main/java/com/sk89q/worldedit/util/CommandPermissionsHandler.java b/src/main/java/com/sk89q/worldedit/util/CommandPermissionsHandler.java new file mode 100644 index 000000000..e558dec59 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/CommandPermissionsHandler.java @@ -0,0 +1,41 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.worldedit.util.command.parametric.PermissionsHandler; +import com.sk89q.worldedit.LocalPlayer; + +public class CommandPermissionsHandler extends PermissionsHandler { + + public CommandPermissionsHandler() { + } + + @Override + protected boolean hasPermission(CommandContext context, String permission) { + LocalPlayer sender = context.getLocals().get(LocalPlayer.class); + if (sender == null) { + return true; + } else { + return sender.hasPermission(permission); + } + } + +} diff --git a/src/main/java/com/sk89q/worldedit/util/WorldEditBinding.java b/src/main/java/com/sk89q/worldedit/util/WorldEditBinding.java new file mode 100644 index 000000000..835d88c8f --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/WorldEditBinding.java @@ -0,0 +1,198 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util; + +import com.sk89q.worldedit.*; +import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.function.mask.Mask; +import com.sk89q.worldedit.function.pattern.Pattern; +import com.sk89q.worldedit.internal.annotation.Direction; +import com.sk89q.worldedit.internal.annotation.Selection; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.util.command.parametric.*; + +/** + * Binds standard WorldEdit classes such as {@link Player} and {@link LocalSession}. + */ +public class WorldEditBinding extends BindingHelper { + + private final WorldEdit worldEdit; + + /** + * Create a new instance. + * + * @param worldEdit the WorldEdit instance to bind to + */ + public WorldEditBinding(WorldEdit worldEdit) { + this.worldEdit = worldEdit; + } + + /** + * Gets a selection from a {@link ArgumentStack}. + * + * @param context the context + * @param selection the annotation + * @return a selection + * @throws IncompleteRegionException if no selection is available + * @throws ParameterException on other error + */ + @BindingMatch(classifier = Selection.class, + type = Region.class, + behavior = BindingBehavior.PROVIDES) + public Object getSelection(ArgumentStack context, Selection selection) throws IncompleteRegionException, ParameterException { + Player sender = getPlayer(context); + LocalSession session = worldEdit.getSessionManager().get(sender); + return session.getSelection(sender.getWorld()); + } + + /** + * Gets an {@link EditSession} from a {@link ArgumentStack}. + * + * @param context the context + * @return an edit session + * @throws ParameterException on other error + */ + @BindingMatch(type = EditSession.class, + behavior = BindingBehavior.PROVIDES) + public EditSession getEditSession(ArgumentStack context) throws ParameterException { + Player sender = getPlayer(context); + LocalSession session = worldEdit.getSessionManager().get(sender); + EditSession editSession = session.createEditSession(sender); + editSession.enableQueue(); + context.getContext().getLocals().put(EditSession.class, editSession); + session.tellVersion(sender); + return editSession; + } + + /** + * Gets an {@link LocalSession} from a {@link ArgumentStack}. + * + * @param context the context + * @return a local session + * @throws ParameterException on error + */ + @BindingMatch(type = LocalSession.class, + behavior = BindingBehavior.PROVIDES) + public LocalSession getLocalSession(ArgumentStack context) throws ParameterException { + Player sender = getPlayer(context); + return worldEdit.getSessionManager().get(sender); + } + + /** + * Gets an {@link Player} from a {@link ArgumentStack}. + * + * @param context the context + * @return a local player + * @throws ParameterException on error + */ + @BindingMatch(type = Player.class, + behavior = BindingBehavior.PROVIDES) + public Player getPlayer(ArgumentStack context) throws ParameterException { + Actor sender = context.getContext().getLocals().get(Actor.class); + if (sender == null) { + throw new ParameterException("No player to get a session for"); + } else if (sender instanceof Player) { + return (Player) sender; + } else { + throw new ParameterException("Caller is not a player"); + } + } + + /** + * Gets an {@link Player} from a {@link ArgumentStack}. + * + * @param context the context + * @return a local player + * @throws ParameterException on error + */ + @SuppressWarnings("deprecation") + @BindingMatch(type = LocalPlayer.class, + behavior = BindingBehavior.PROVIDES) + public Player getLocalPlayer(ArgumentStack context) throws ParameterException { + Player player = getPlayer(context); + if (player instanceof LocalPlayer) { + return (LocalPlayer) player; + } else { + throw new ParameterException("This command/function needs to be updated to take 'Player' rather than 'LocalPlayer'"); + } + } + + /** + * Gets an {@link Pattern} from a {@link ArgumentStack}. + * + * @param context the context + * @return a pattern + * @throws ParameterException on error + * @throws WorldEditException on error + */ + @BindingMatch(type = Pattern.class, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1) + public Pattern getPattern(ArgumentStack context) throws ParameterException, WorldEditException { + Actor actor = context.getContext().getLocals().get(Actor.class); + ParserContext parserContext = new ParserContext(); + parserContext.setActor(context.getContext().getLocals().get(Actor.class)); + parserContext.setWorld(actor.getWorld()); + parserContext.setSession(worldEdit.getSessionManager().get(actor)); + return worldEdit.getPatternRegistry().parseFromInput(context.next(), parserContext); + } + + /** + * Gets an {@link Mask} from a {@link ArgumentStack}. + * + * @param context the context + * @return a pattern + * @throws ParameterException on error + * @throws WorldEditException on error + */ + @BindingMatch(type = Mask.class, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1) + public Mask getMask(ArgumentStack context) throws ParameterException, WorldEditException { + Actor actor = context.getContext().getLocals().get(Actor.class); + ParserContext parserContext = new ParserContext(); + parserContext.setActor(context.getContext().getLocals().get(Actor.class)); + parserContext.setWorld(actor.getWorld()); + parserContext.setSession(worldEdit.getSessionManager().get(actor)); + return worldEdit.getMaskRegistry().parseFromInput(context.next(), parserContext); + } + + /** + * Get a direction from the player. + * + * @param context the context + * @param direction the direction annotation + * @return a pattern + * @throws ParameterException on error + * @throws UnknownDirectionException on an unknown direction + */ + @BindingMatch(classifier = Direction.class, + type = Vector.class, + behavior = BindingBehavior.CONSUMES, + consumedCount = 1) + public Vector getDirection(ArgumentStack context, Direction direction) + throws ParameterException, UnknownDirectionException { + Player sender = getPlayer(context); + return worldEdit.getDirection(sender, context.next()); + } + +} diff --git a/src/main/java/com/sk89q/worldedit/util/WorldEditExceptionConverter.java b/src/main/java/com/sk89q/worldedit/util/WorldEditExceptionConverter.java new file mode 100644 index 000000000..69187b5af --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/WorldEditExceptionConverter.java @@ -0,0 +1,151 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.worldedit.*; +import com.sk89q.worldedit.blocks.ItemType; +import com.sk89q.worldedit.command.InsufficientArgumentsException; +import com.sk89q.worldedit.internal.expression.ExpressionException; +import com.sk89q.worldedit.regions.RegionOperationException; +import com.sk89q.worldedit.util.command.parametric.ExceptionConverterHelper; +import com.sk89q.worldedit.util.command.parametric.ExceptionMatch; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * converts WorldEdit exceptions and converts them into {@link CommandException}s. + */ +public class WorldEditExceptionConverter extends ExceptionConverterHelper { + + private static final Pattern numberFormat = Pattern.compile("^For input string: \"(.*)\"$"); + private final WorldEdit worldEdit; + + public WorldEditExceptionConverter(WorldEdit worldEdit) { + checkNotNull(worldEdit); + this.worldEdit = worldEdit; + } + + @ExceptionMatch + public void convert(PlayerNeededException e) throws CommandException { + throw new CommandException(e.getMessage()); + } + + @ExceptionMatch + public void convert(NumberFormatException e) throws CommandException { + final Matcher matcher = numberFormat.matcher(e.getMessage()); + + if (matcher.matches()) { + throw new CommandException("Number expected; string \"" + matcher.group(1) + + "\" given."); + } else { + throw new CommandException("Number expected; string given."); + } + } + + @ExceptionMatch + public void convert(IncompleteRegionException e) throws CommandException { + throw new CommandException("Make a region selection first."); + } + + @ExceptionMatch + public void convert(UnknownItemException e) throws CommandException { + throw new CommandException("Block name '" + e.getID() + "' was not recognized."); + } + + @ExceptionMatch + public void convert(InvalidItemException e) throws CommandException { + throw new CommandException(e.getMessage()); + } + + @ExceptionMatch + public void convert(DisallowedItemException e) throws CommandException { + throw new CommandException("Block '" + e.getID() + + "' not allowed (see WorldEdit configuration)."); + } + + @ExceptionMatch + public void convert(MaxChangedBlocksException e) throws CommandException { + throw new CommandException("Max blocks changed in an operation reached (" + + e.getBlockLimit() + ")."); + } + + @ExceptionMatch + public void convert(MaxRadiusException e) throws CommandException { + throw new CommandException("Maximum radius: " + worldEdit.getConfiguration().maxRadius); + } + + @ExceptionMatch + public void convert(UnknownDirectionException e) throws CommandException { + throw new CommandException("Unknown direction: " + e.getDirection()); + } + + @ExceptionMatch + public void convert(InsufficientArgumentsException e) throws CommandException { + throw new CommandException(e.getMessage()); + } + + @ExceptionMatch + public void convert(RegionOperationException e) throws CommandException { + throw new CommandException(e.getMessage()); + } + + @ExceptionMatch + public void convert(ExpressionException e) throws CommandException { + throw new CommandException(e.getMessage()); + } + + @ExceptionMatch + public void convert(EmptyClipboardException e) throws CommandException { + throw new CommandException("Your clipboard is empty. Use //copy first."); + } + + @ExceptionMatch + public void convert(InvalidFilenameException e) throws CommandException { + throw new CommandException("Filename '" + e.getFilename() + "' invalid: " + + e.getMessage()); + } + + @ExceptionMatch + public void convert(FilenameResolutionException e) throws CommandException { + throw new CommandException( + "File '" + e.getFilename() + "' resolution error: " + e.getMessage()); + } + + @ExceptionMatch + public void convert(InvalidToolBindException e) throws CommandException { + throw new CommandException("Can't bind tool to " + + ItemType.toHeldName(e.getItemId()) + ": " + e.getMessage()); + } + + @ExceptionMatch + public void convert(FileSelectionAbortedException e) throws CommandException { + throw new CommandException("File selection aborted."); + } + + @ExceptionMatch + public void convert(WorldEditException e) throws CommandException { + throw new CommandException(e.getMessage()); + } + +} diff --git a/src/main/java/com/sk89q/worldedit/util/command/CommandCallable.java b/src/main/java/com/sk89q/worldedit/util/command/CommandCallable.java new file mode 100644 index 000000000..4c0046ba0 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/CommandCallable.java @@ -0,0 +1,64 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; + +import java.util.Collection; +import java.util.Set; + +/** + * 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/worldedit/util/command/CommandMapping.java b/src/main/java/com/sk89q/worldedit/util/command/CommandMapping.java new file mode 100644 index 000000000..a6f973cb1 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/CommandMapping.java @@ -0,0 +1,79 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/Description.java b/src/main/java/com/sk89q/worldedit/util/command/Description.java new file mode 100644 index 000000000..e45885ed1 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/Description.java @@ -0,0 +1,70 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/Dispatcher.java b/src/main/java/com/sk89q/worldedit/util/command/Dispatcher.java new file mode 100644 index 000000000..90beed347 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/Dispatcher.java @@ -0,0 +1,113 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.CommandLocals; + +import java.util.Collection; + +/** + * 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/worldedit/util/command/InvalidUsageException.java b/src/main/java/com/sk89q/worldedit/util/command/InvalidUsageException.java new file mode 100644 index 000000000..a97ce4d2f --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/InvalidUsageException.java @@ -0,0 +1,49 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/MissingParameterException.java b/src/main/java/com/sk89q/worldedit/util/command/MissingParameterException.java new file mode 100644 index 000000000..8080929e3 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/MissingParameterException.java @@ -0,0 +1,31 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command; + +import com.sk89q.worldedit.util.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/worldedit/util/command/Parameter.java b/src/main/java/com/sk89q/worldedit/util/command/Parameter.java new file mode 100644 index 000000000..868c3840a --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/Parameter.java @@ -0,0 +1,66 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/SimpleDescription.java b/src/main/java/com/sk89q/worldedit/util/command/SimpleDescription.java new file mode 100644 index 000000000..38ab1b799 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/SimpleDescription.java @@ -0,0 +1,129 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/SimpleDispatcher.java b/src/main/java/com/sk89q/worldedit/util/command/SimpleDispatcher.java new file mode 100644 index 000000000..af379e7b4 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/SimpleDispatcher.java @@ -0,0 +1,122 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command; + +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; + +import java.util.*; + +/** + * 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/worldedit/util/command/SimpleDispatcherCommand.java b/src/main/java/com/sk89q/worldedit/util/command/SimpleDispatcherCommand.java new file mode 100644 index 000000000..ec7b1545e --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/SimpleDispatcherCommand.java @@ -0,0 +1,96 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; + +import java.util.*; + +/** + * 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/worldedit/util/command/SimpleParameter.java b/src/main/java/com/sk89q/worldedit/util/command/SimpleParameter.java new file mode 100644 index 000000000..59fc8dddd --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/SimpleParameter.java @@ -0,0 +1,131 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/UnconsumedParameterException.java b/src/main/java/com/sk89q/worldedit/util/command/UnconsumedParameterException.java new file mode 100644 index 000000000..360a5408c --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/UnconsumedParameterException.java @@ -0,0 +1,42 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command; + +import com.sk89q.worldedit.util.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/worldedit/util/command/binding/PrimitiveBindings.java b/src/main/java/com/sk89q/worldedit/util/command/binding/PrimitiveBindings.java new file mode 100644 index 000000000..a4ed7ab0c --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/binding/PrimitiveBindings.java @@ -0,0 +1,255 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.binding; + +import com.sk89q.worldedit.util.command.parametric.*; + +import java.lang.annotation.Annotation; + +/** + * 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/worldedit/util/command/binding/Range.java b/src/main/java/com/sk89q/worldedit/util/command/binding/Range.java new file mode 100644 index 000000000..0ff21ca12 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/binding/Range.java @@ -0,0 +1,50 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/binding/StandardBindings.java b/src/main/java/com/sk89q/worldedit/util/command/binding/StandardBindings.java new file mode 100644 index 000000000..a45b88a41 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/binding/StandardBindings.java @@ -0,0 +1,46 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.binding; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.worldedit.util.command.parametric.BindingBehavior; +import com.sk89q.worldedit.util.command.parametric.BindingHelper; +import com.sk89q.worldedit.util.command.parametric.BindingMatch; +import com.sk89q.worldedit.util.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/worldedit/util/command/binding/Switch.java b/src/main/java/com/sk89q/worldedit/util/command/binding/Switch.java new file mode 100644 index 000000000..34c25cad5 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/binding/Switch.java @@ -0,0 +1,44 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/binding/Text.java b/src/main/java/com/sk89q/worldedit/util/command/binding/Text.java new file mode 100644 index 000000000..030bc9ad4 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/binding/Text.java @@ -0,0 +1,41 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.binding; + +import com.sk89q.worldedit.util.command.parametric.ArgumentStack; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 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/worldedit/util/command/binding/Validate.java b/src/main/java/com/sk89q/worldedit/util/command/binding/Validate.java new file mode 100644 index 000000000..3686aa359 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/binding/Validate.java @@ -0,0 +1,45 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/fluent/CommandGraph.java b/src/main/java/com/sk89q/worldedit/util/command/fluent/CommandGraph.java new file mode 100644 index 000000000..6e5b189f7 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/fluent/CommandGraph.java @@ -0,0 +1,84 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.fluent; + +import com.sk89q.worldedit.util.command.Dispatcher; +import com.sk89q.worldedit.util.command.SimpleDispatcher; +import com.sk89q.worldedit.util.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/worldedit/util/command/fluent/DispatcherNode.java b/src/main/java/com/sk89q/worldedit/util/command/fluent/DispatcherNode.java new file mode 100644 index 000000000..cf0d93d74 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/fluent/DispatcherNode.java @@ -0,0 +1,142 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.fluent; + +import com.sk89q.worldedit.util.command.CommandCallable; +import com.sk89q.worldedit.util.command.Dispatcher; +import com.sk89q.worldedit.util.command.SimpleDispatcher; +import com.sk89q.worldedit.util.command.SimpleDispatcherCommand; +import com.sk89q.worldedit.util.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.worldedit.util.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/worldedit/util/command/parametric/AbstractInvokeListener.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/AbstractInvokeListener.java new file mode 100644 index 000000000..65fadc101 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/AbstractInvokeListener.java @@ -0,0 +1,36 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.worldedit.util.command.SimpleDescription; + +import java.lang.reflect.Method; + +/** + * 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/worldedit/util/command/parametric/ArgumentStack.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ArgumentStack.java new file mode 100644 index 000000000..0f0b9a52b --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ArgumentStack.java @@ -0,0 +1,78 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/parametric/Binding.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/Binding.java new file mode 100644 index 000000000..dcf931c81 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/Binding.java @@ -0,0 +1,92 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.worldedit.util.command.binding.PrimitiveBindings; +import com.sk89q.worldedit.util.command.binding.StandardBindings; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * 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/worldedit/util/command/parametric/BindingBehavior.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/BindingBehavior.java new file mode 100644 index 000000000..e194bce56 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/BindingBehavior.java @@ -0,0 +1,52 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandLocals; +import com.sk89q.worldedit.util.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/worldedit/util/command/parametric/BindingHelper.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/BindingHelper.java new file mode 100644 index 000000000..a104d6776 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/BindingHelper.java @@ -0,0 +1,224 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandException; + +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; + +/** + * 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/worldedit/util/command/parametric/BindingMatch.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/BindingMatch.java new file mode 100644 index 000000000..049d3dc4d --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/BindingMatch.java @@ -0,0 +1,71 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/parametric/ContextArgumentStack.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ContextArgumentStack.java new file mode 100644 index 000000000..c7222b9e1 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ContextArgumentStack.java @@ -0,0 +1,178 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.worldedit.util.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/worldedit/util/command/parametric/ExceptionConverter.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ExceptionConverter.java new file mode 100644 index 000000000..1eb9491ca --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ExceptionConverter.java @@ -0,0 +1,52 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/parametric/ExceptionConverterHelper.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ExceptionConverterHelper.java new file mode 100644 index 000000000..8a72f1a33 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ExceptionConverterHelper.java @@ -0,0 +1,109 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.minecraft.util.commands.WrappedCommandException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 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/worldedit/util/command/parametric/ExceptionMatch.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ExceptionMatch.java new file mode 100644 index 000000000..619bc9d46 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ExceptionMatch.java @@ -0,0 +1,34 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/parametric/InvokeHandler.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/InvokeHandler.java new file mode 100644 index 000000000..465f89689 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/InvokeHandler.java @@ -0,0 +1,82 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; + +import java.lang.reflect.Method; + +/** + * 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/worldedit/util/command/parametric/InvokeListener.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/InvokeListener.java new file mode 100644 index 000000000..f30f86c9f --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/InvokeListener.java @@ -0,0 +1,58 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandPermissions; +import com.sk89q.worldedit.util.command.CommandCallable; +import com.sk89q.worldedit.util.command.SimpleDescription; + +import java.lang.reflect.Method; + +/** + * 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/worldedit/util/command/parametric/LegacyCommandsHandler.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/LegacyCommandsHandler.java new file mode 100644 index 000000000..4602be0ab --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/LegacyCommandsHandler.java @@ -0,0 +1,97 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.Command; +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.minecraft.util.commands.CommandException; +import com.sk89q.worldedit.util.command.MissingParameterException; +import com.sk89q.worldedit.util.command.SimpleDescription; +import com.sk89q.worldedit.util.command.UnconsumedParameterException; + +import java.lang.reflect.Method; + +/** + * 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/worldedit/util/command/parametric/Optional.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/Optional.java new file mode 100644 index 000000000..bd6d5448f --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/Optional.java @@ -0,0 +1,41 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/parametric/ParameterData.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParameterData.java new file mode 100644 index 000000000..649c576b4 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParameterData.java @@ -0,0 +1,194 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.worldedit.util.command.SimpleParameter; +import com.sk89q.worldedit.util.command.binding.PrimitiveBindings; +import com.sk89q.worldedit.util.command.binding.Range; +import com.sk89q.worldedit.util.command.binding.Text; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +/** + * 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/worldedit/util/command/parametric/ParameterException.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParameterException.java new file mode 100644 index 000000000..8dc924f15 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParameterException.java @@ -0,0 +1,45 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/parametric/ParametricBuilder.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParametricBuilder.java new file mode 100644 index 000000000..be7bc2c1e --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParametricBuilder.java @@ -0,0 +1,205 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +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.worldedit.util.command.CommandCallable; +import com.sk89q.worldedit.util.command.Dispatcher; +import com.sk89q.worldedit.util.command.binding.PrimitiveBindings; +import com.sk89q.worldedit.util.command.binding.StandardBindings; +import com.sk89q.worldedit.util.command.binding.Switch; +import com.thoughtworks.paranamer.CachingParanamer; +import com.thoughtworks.paranamer.Paranamer; + +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; + +/** + * 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/worldedit/util/command/parametric/ParametricCallable.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParametricCallable.java new file mode 100644 index 000000000..1d1154d13 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParametricCallable.java @@ -0,0 +1,538 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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.worldedit.util.command.CommandCallable; +import com.sk89q.worldedit.util.command.InvalidUsageException; +import com.sk89q.worldedit.util.command.MissingParameterException; +import com.sk89q.worldedit.util.command.Parameter; +import com.sk89q.worldedit.util.command.SimpleDescription; +import com.sk89q.worldedit.util.command.UnconsumedParameterException; +import com.sk89q.worldedit.util.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/worldedit/util/command/parametric/ParametricException.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParametricException.java new file mode 100644 index 000000000..f2c826d73 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/ParametricException.java @@ -0,0 +1,46 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.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/worldedit/util/command/parametric/PermissionsHandler.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/PermissionsHandler.java new file mode 100644 index 000000000..ed11942ba --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/PermissionsHandler.java @@ -0,0 +1,67 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +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; + +import java.lang.reflect.Method; + +/** + * 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/worldedit/util/command/parametric/StringArgumentStack.java b/src/main/java/com/sk89q/worldedit/util/command/parametric/StringArgumentStack.java new file mode 100644 index 000000000..7d93070fa --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/util/command/parametric/StringArgumentStack.java @@ -0,0 +1,128 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) 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 Foundation, either version 3 of the License, or + * (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 PARTICULAR PURPOSE. See the GNU Lesser 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.worldedit.util.command.parametric; + +import com.sk89q.minecraft.util.commands.CommandContext; +import com.sk89q.worldedit.util.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; + } + +}