From ad62dbe565e5a1b1d2005073c195ae008c67b2d5 Mon Sep 17 00:00:00 2001 From: sk89q Date: Thu, 24 Feb 2011 16:39:23 -0800 Subject: [PATCH] Refactored command handling code to be more reusable. --- .../minecraft/util/commands/Command.java | 36 ++++ .../util/commands/CommandException.java | 37 ++++ .../util/commands/CommandPermissions.java | 9 + .../commands/CommandPermissionsException.java | 29 +++ .../util/commands/CommandUsageException.java | 35 ++++ .../util/commands/CommandsManager.java | 180 +++++++++++------- .../MissingNestedCommandException.java | 29 +++ .../util/commands/NestedCommand.java | 13 ++ ...er.java => UnhandledCommandException.java} | 6 +- .../commands/WrappedCommandException.java | 28 +++ src/com/sk89q/worldedit/LocalPlayer.java | 3 +- src/com/sk89q/worldedit/WorldEdit.java | 31 ++- 12 files changed, 354 insertions(+), 82 deletions(-) create mode 100644 src/com/sk89q/minecraft/util/commands/CommandException.java create mode 100644 src/com/sk89q/minecraft/util/commands/CommandPermissionsException.java create mode 100644 src/com/sk89q/minecraft/util/commands/CommandUsageException.java create mode 100644 src/com/sk89q/minecraft/util/commands/MissingNestedCommandException.java rename src/com/sk89q/minecraft/util/commands/{CommandsPlayer.java => UnhandledCommandException.java} (82%) create mode 100644 src/com/sk89q/minecraft/util/commands/WrappedCommandException.java diff --git a/src/com/sk89q/minecraft/util/commands/Command.java b/src/com/sk89q/minecraft/util/commands/Command.java index b485803d2..381446135 100644 --- a/src/com/sk89q/minecraft/util/commands/Command.java +++ b/src/com/sk89q/minecraft/util/commands/Command.java @@ -21,12 +21,48 @@ package com.sk89q.minecraft.util.commands; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +/** + * This annotation indicates a command. Methods should be marked with this + * annotation to tell {@link CommandsManager} that the method is a command. + * Note that the method name can actually be anything. + * + * @author sk89q + */ @Retention(RetentionPolicy.RUNTIME) public @interface Command { + /** + * A list of aliases for the command. The first alias is the most + * important -- it is the main name of the command. (The method name + * is never used for anything). + */ String[] aliases(); + + /** + * Usage instruction. Example text for usage could be + * [-h] [name] [message]. + */ String usage() default ""; + + /** + * A short description for the command. + */ String desc(); + + /** + * The minimum number of arguments. This should be 0 or above. + */ int min() default 0; + + /** + * The maximum number of arguments. Use -1 for an unlimited number + * of arguments. + */ int max() default -1; + + /** + * Flags allow special processing for flags such as -h in the command, + * allowing users to easily turn on a flag. This is a string with + * each character being a flag. Use A-Z and a-z as possible flags. + */ String flags() default ""; } diff --git a/src/com/sk89q/minecraft/util/commands/CommandException.java b/src/com/sk89q/minecraft/util/commands/CommandException.java new file mode 100644 index 000000000..641587afa --- /dev/null +++ b/src/com/sk89q/minecraft/util/commands/CommandException.java @@ -0,0 +1,37 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.minecraft.util.commands; + +public class CommandException extends Exception { + private static final long serialVersionUID = 870638193072101739L; + + public CommandException() { + super(); + } + + public CommandException(String message) { + super(message); + } + + public CommandException(Throwable t) { + super(t); + } + +} diff --git a/src/com/sk89q/minecraft/util/commands/CommandPermissions.java b/src/com/sk89q/minecraft/util/commands/CommandPermissions.java index 88a7a97e9..7d3383d11 100644 --- a/src/com/sk89q/minecraft/util/commands/CommandPermissions.java +++ b/src/com/sk89q/minecraft/util/commands/CommandPermissions.java @@ -22,7 +22,16 @@ package com.sk89q.minecraft.util.commands; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +/** + * Indicates a list of permissions that should be checked. + * + * @author sk89q + */ @Retention(RetentionPolicy.RUNTIME) public @interface CommandPermissions { + /** + * A list of permissions. Only one permission has to be met + * for the command to be permitted. + */ String[] value(); } diff --git a/src/com/sk89q/minecraft/util/commands/CommandPermissionsException.java b/src/com/sk89q/minecraft/util/commands/CommandPermissionsException.java new file mode 100644 index 000000000..a4e5bc693 --- /dev/null +++ b/src/com/sk89q/minecraft/util/commands/CommandPermissionsException.java @@ -0,0 +1,29 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.minecraft.util.commands; + +/** + * Thrown when not enough permissions are satisfied. + * + * @author sk89q + */ +public class CommandPermissionsException extends CommandException { + private static final long serialVersionUID = -602374621030168291L; +} diff --git a/src/com/sk89q/minecraft/util/commands/CommandUsageException.java b/src/com/sk89q/minecraft/util/commands/CommandUsageException.java new file mode 100644 index 000000000..35ec61330 --- /dev/null +++ b/src/com/sk89q/minecraft/util/commands/CommandUsageException.java @@ -0,0 +1,35 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.minecraft.util.commands; + +public class CommandUsageException extends CommandException { + private static final long serialVersionUID = -6761418114414516542L; + + protected String usage; + + public CommandUsageException(String message, String usage) { + super(message); + this.usage = usage; + } + + public String getUsage() { + return usage; + } +} diff --git a/src/com/sk89q/minecraft/util/commands/CommandsManager.java b/src/com/sk89q/minecraft/util/commands/CommandsManager.java index c3deb80fe..bd725b1fe 100644 --- a/src/com/sk89q/minecraft/util/commands/CommandsManager.java +++ b/src/com/sk89q/minecraft/util/commands/CommandsManager.java @@ -28,24 +28,51 @@ import java.util.Set; import com.sk89q.util.StringUtil; /** - * Manager for handling commands. + * Manager for handling commands. This allows you to easily process commands, + * including nested commands, by correctly annotating methods of a class. + * The commands are thus declaratively defined, and it's easy to spot + * how permissions and commands work out, and it decreases the opportunity + * for errors because the consistency would cause any odd balls to show. + * The manager also handles some boilerplate code such as number of arguments + * checking and printing usage. + * + *

To use this, it is merely a matter of registering classes containing + * the commands (as methods with the proper annotations) with the + * manager. When you want to process a command, use one of the + * execute methods. If something is wrong, such as incorrect + * usage, insufficient permissions, or a missing command altogether, an + * exception will be raised for upstream handling. + * + *

To mark a method as a command, use {@link Command}. For nested commands, + * see {@link NestedCommand}. To handle permissions, use + * {@link CommandPermissions}. + * + *

This uses Java reflection extensively, but to reduce the overhead of + * reflection, command lookups are completely cached on registration. This + * allows for fast command handling. Method invocation still has to be done + * with reflection, but this is quite fast in that of itself. * * @author sk89q + * @param command sender class */ -public class CommandsManager { +public abstract class CommandsManager { + /** - * Mapping of nested commands (including aliases) with a description. + * Mapping of commands (including aliases) with a description. Root + * commands are stored under a key of null, whereas child commands are + * cached under their respective {@link Method}. */ - public Map> commands + protected Map> commands = new HashMap>(); + /** * Mapping of commands (not including aliases) with a description. */ - public Map descs = new HashMap(); + protected Map descs = new HashMap(); /** - * Register an object that contains commands (denoted by the - * com.sk89q.util.commands.Command annotation. The methods are + * Register an object that contains commands (denoted by + * {@link Command}. The methods are * cached into a map for later usage and it reduces the overhead of * reflection (method lookup via reflection is relatively slow). * @@ -109,7 +136,8 @@ public class CommandsManager { } /** - * Checks to see whether there is a command. + * Checks to see whether there is a command named such at the root level. + * This will check aliases as well. * * @param command * @return @@ -119,7 +147,7 @@ public class CommandsManager { } /** - * Get a list of command descriptions. + * Get a list of command descriptions. This is only for root commands. * * @return */ @@ -135,7 +163,7 @@ public class CommandsManager { * @param cmd * @return */ - private String getUsage(String[] args, int level, Command cmd) { + protected String getUsage(String[] args, int level, Command cmd) { StringBuilder command = new StringBuilder(); command.append("/"); @@ -156,11 +184,13 @@ public class CommandsManager { * @param args * @param level * @param method - * @param palyer + * @param player * @return + * @throws CommandException */ - private String getNestedUsage(String[] args, int level, Method method, - CommandsPlayer player) { + protected String getNestedUsage(String[] args, int level, + Method method, T player) throws CommandException { + StringBuilder command = new StringBuilder(); command.append("/"); @@ -171,6 +201,7 @@ public class CommandsManager { Map map = commands.get(method); + boolean found = false; command.append("<"); @@ -178,6 +209,7 @@ public class CommandsManager { for (Map.Entry entry : map.entrySet()) { Method childMethod = entry.getValue(); + found = true; if (hasPermission(childMethod, player)) { Command childCmd = childMethod.getAnnotation(Command.class); @@ -189,7 +221,12 @@ public class CommandsManager { if (allowedCommands.size() > 0) { command.append(StringUtil.joinString(allowedCommands, "|", 0)); } else { - command.append("action"); + if (!found) { + command.append("?"); + } else { + //command.append("action"); + throw new CommandPermissionsException(); + } } command.append(">"); @@ -197,18 +234,42 @@ public class CommandsManager { return command.toString(); } + /** + * Attempt to execute a command. This version takes a separate command + * name (for the root command) and then a list of following arguments. + * + * @param cmd command to run + * @param args arguments + * @param player command source + * @param methodArgs method arguments + * @throws CommandException + */ + public void execute(String cmd, String[] args, T player, + Object ... methodArgs) throws CommandException { + + String[] newArgs = new String[args.length + 1]; + System.arraycopy(args, 0, newArgs, 1, args.length); + newArgs[0] = cmd; + Object[] newMethodArgs = new Object[methodArgs.length + 1]; + System.arraycopy(methodArgs, 0, newMethodArgs, 1, methodArgs.length); + + executeMethod(null, newArgs, player, newMethodArgs, 0); + } + /** * Attempt to execute a command. * * @param args * @param player * @param methodArgs - * @return - * @throws Throwable + * @throws CommandException */ - public boolean execute(String[] args, CommandsPlayer player, - Object[] methodArgs) throws Throwable { - return executeMethod(null, args, player, methodArgs, 0); + public void execute(String[] args, T player, + Object ... methodArgs) throws CommandException { + + Object[] newMethodArgs = new Object[methodArgs.length + 1]; + System.arraycopy(methodArgs, 0, newMethodArgs, 1, methodArgs.length); + executeMethod(null, args, player, newMethodArgs, 0); } /** @@ -219,12 +280,11 @@ public class CommandsManager { * @param player * @param methodArgs * @param level - * @return - * @throws Throwable + * @throws CommandException */ - public boolean executeMethod(Method parent, String[] args, - CommandsPlayer player, Object[] methodArgs, int level) - throws Throwable { + public void executeMethod(Method parent, String[] args, + T player, Object[] methodArgs, int level) throws CommandException { + String cmdName = args[level]; Map map = commands.get(parent); @@ -232,25 +292,25 @@ public class CommandsManager { if (method == null) { if (parent == null) { // Root - return false; + throw new UnhandledCommandException(); } else { - player.printError(getNestedUsage(args, level - 1, parent, player)); - return true; + throw new MissingNestedCommandException("Unknown command: " + cmdName, + getNestedUsage(args, level - 1, parent, player)); } } - if (!checkPermissions(method, player)) { - return true; + if (!hasPermission(method, player)) { + throw new CommandPermissionsException(); } int argsCount = args.length - 1 - level; if (method.isAnnotationPresent(NestedCommand.class)) { if (argsCount == 0) { - player.printError(getNestedUsage(args, level, method, player)); - return true; + throw new MissingNestedCommandException("Sub-command required.", + getNestedUsage(args, level, method, player)); } else { - return executeMethod(method, args, player, methodArgs, level + 1); + executeMethod(method, args, player, methodArgs, level + 1); } } else { Command cmd = method.getAnnotation(Command.class); @@ -261,22 +321,19 @@ public class CommandsManager { CommandContext context = new CommandContext(newArgs); if (context.argsLength() < cmd.min()) { - player.printError("Too few arguments."); - player.printError(getUsage(args, level, cmd)); - return true; + throw new CommandUsageException("Too few arguments.", + getUsage(args, level, cmd)); } if (cmd.max() != -1 && context.argsLength() > cmd.max()) { - player.printError("Too many arguments."); - player.printError(getUsage(args, level, cmd)); - return true; + throw new CommandUsageException("Too many arguments.", + getUsage(args, level, cmd)); } for (char flag : context.getFlags()) { if (cmd.flags().indexOf(String.valueOf(flag)) == -1) { - player.printError("Unknown flag: " + flag); - player.printError(getUsage(args, level, cmd)); - return true; + throw new CommandUsageException("Unknown flag: " + flag, + getUsage(args, level, cmd)); } } @@ -289,35 +346,9 @@ public class CommandsManager { } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { - throw e.getCause(); + throw new WrappedCommandException(e.getCause()); } } - - return true; - } - - /** - * Checks permissions, prints an error if needed. - * - * @param method - * @param player - * @return - */ - private boolean checkPermissions(Method method, CommandsPlayer player) { - if (!method.isAnnotationPresent(CommandPermissions.class)) { - return true; - } - - CommandPermissions perms = method.getAnnotation(CommandPermissions.class); - - for (String perm : perms.value()) { - if (player.hasPermission(perm)) { - return true; - } - } - - player.printError("You don't have permission for this command."); - return false; } /** @@ -327,18 +358,27 @@ public class CommandsManager { * @param player * @return */ - private boolean hasPermission(Method method, CommandsPlayer player) { + protected boolean hasPermission(Method method, T player) { CommandPermissions perms = method.getAnnotation(CommandPermissions.class); if (perms == null) { return true; } for (String perm : perms.value()) { - if (player.hasPermission(perm)) { + if (hasPermission(player, perm)) { return true; } } return false; } + + /** + * Returns whether a player permission.. + * + * @param player + * @param perm + * @return + */ + public abstract boolean hasPermission(T player, String perm); } diff --git a/src/com/sk89q/minecraft/util/commands/MissingNestedCommandException.java b/src/com/sk89q/minecraft/util/commands/MissingNestedCommandException.java new file mode 100644 index 000000000..f13fffc46 --- /dev/null +++ b/src/com/sk89q/minecraft/util/commands/MissingNestedCommandException.java @@ -0,0 +1,29 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.minecraft.util.commands; + +public class MissingNestedCommandException extends CommandUsageException { + private static final long serialVersionUID = -4382896182979285355L; + + public MissingNestedCommandException(String message, String usage) { + super(message, usage); + } + +} diff --git a/src/com/sk89q/minecraft/util/commands/NestedCommand.java b/src/com/sk89q/minecraft/util/commands/NestedCommand.java index 5f12a76b9..84f2bc5e4 100644 --- a/src/com/sk89q/minecraft/util/commands/NestedCommand.java +++ b/src/com/sk89q/minecraft/util/commands/NestedCommand.java @@ -22,7 +22,20 @@ package com.sk89q.minecraft.util.commands; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +/** + * Indicates a nested command. Mark methods with this annotation to tell + * {@link CommandsManager} that a method is merely a shell for child + * commands. Note that the body of a method marked with this annotation + * will never called. Additionally, not all fields of {@link Command} apply + * when it is used in conjunction with this annotation, although both + * are still required. + * + * @author sk89q + */ @Retention(RetentionPolicy.RUNTIME) public @interface NestedCommand { + /** + * A list of classes with the child commands. + */ Class[] value(); } diff --git a/src/com/sk89q/minecraft/util/commands/CommandsPlayer.java b/src/com/sk89q/minecraft/util/commands/UnhandledCommandException.java similarity index 82% rename from src/com/sk89q/minecraft/util/commands/CommandsPlayer.java rename to src/com/sk89q/minecraft/util/commands/UnhandledCommandException.java index dd651c499..b56334cbb 100644 --- a/src/com/sk89q/minecraft/util/commands/CommandsPlayer.java +++ b/src/com/sk89q/minecraft/util/commands/UnhandledCommandException.java @@ -19,7 +19,7 @@ package com.sk89q.minecraft.util.commands; -public interface CommandsPlayer { - public void printError(String msg); - public boolean hasPermission(String perm); +public class UnhandledCommandException extends CommandException { + private static final long serialVersionUID = 3370887306593968091L; + } diff --git a/src/com/sk89q/minecraft/util/commands/WrappedCommandException.java b/src/com/sk89q/minecraft/util/commands/WrappedCommandException.java new file mode 100644 index 000000000..bd0eb1dd6 --- /dev/null +++ b/src/com/sk89q/minecraft/util/commands/WrappedCommandException.java @@ -0,0 +1,28 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.minecraft.util.commands; + +public class WrappedCommandException extends CommandException { + private static final long serialVersionUID = -4075721444847778918L; + + public WrappedCommandException(Throwable t) { + super(t); + } +} diff --git a/src/com/sk89q/worldedit/LocalPlayer.java b/src/com/sk89q/worldedit/LocalPlayer.java index a96e5e106..f603f61da 100644 --- a/src/com/sk89q/worldedit/LocalPlayer.java +++ b/src/com/sk89q/worldedit/LocalPlayer.java @@ -20,7 +20,6 @@ package com.sk89q.worldedit; import java.io.File; -import com.sk89q.minecraft.util.commands.CommandsPlayer; import com.sk89q.worldedit.bags.BlockBag; import com.sk89q.worldedit.blocks.BlockType; import com.sk89q.worldedit.util.TargetBlock; @@ -29,7 +28,7 @@ import com.sk89q.worldedit.util.TargetBlock; * * @author sk89q */ -public abstract class LocalPlayer implements CommandsPlayer { +public abstract class LocalPlayer { /** * Server. */ diff --git a/src/com/sk89q/worldedit/WorldEdit.java b/src/com/sk89q/worldedit/WorldEdit.java index 106800f58..1606af1da 100644 --- a/src/com/sk89q/worldedit/WorldEdit.java +++ b/src/com/sk89q/worldedit/WorldEdit.java @@ -23,7 +23,12 @@ import java.util.*; import java.util.logging.Logger; import java.io.*; import javax.script.ScriptException; +import com.sk89q.minecraft.util.commands.CommandPermissionsException; +import com.sk89q.minecraft.util.commands.CommandUsageException; import com.sk89q.minecraft.util.commands.CommandsManager; +import com.sk89q.minecraft.util.commands.MissingNestedCommandException; +import com.sk89q.minecraft.util.commands.UnhandledCommandException; +import com.sk89q.minecraft.util.commands.WrappedCommandException; import com.sk89q.util.StringUtil; import com.sk89q.worldedit.LocalSession.CompassMode; import com.sk89q.worldedit.bags.BlockBag; @@ -67,7 +72,7 @@ public class WorldEdit { /** * List of commands. */ - private CommandsManager commands; + private CommandsManager commands; /** * Stores a list of WorldEdit sessions, keyed by players' names. Sessions @@ -96,7 +101,12 @@ public class WorldEdit { this.server = server; this.config = config; - commands = new CommandsManager(); + commands = new CommandsManager() { + @Override + public boolean hasPermission(LocalPlayer player, String perm) { + return player.hasPermission(perm); + } + }; commands.register(ChunkCommands.class); commands.register(ClipboardCommands.class); @@ -886,12 +896,19 @@ public class WorldEdit { logger.info("WorldEdit: " + player.getName() + ": " + StringUtil.joinString(split, " ")); } - - Object[] methodArgs = new Object[] { - null, this, session, player, editSession - }; - return commands.execute(split, player, methodArgs); + commands.execute(split, player, this, 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 (WrappedCommandException e) { + throw e.getCause(); + } catch (UnhandledCommandException e) { + return false; } finally { session.remember(editSession); editSession.flushQueue();