+ *
+ * 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();