/worldedit-core/src/main/java/com/sk89q/minecraft/util/commands/CommandsManager.java
Java | 591 lines | 289 code | 87 blank | 215 comment | 67 complexity | 34066d693884cb5c3461988c5b9181f4 MD5 | raw file
- /*
- * WorldEdit, a Minecraft world manipulation toolkit
- * Copyright (C) sk89q <http://www.sk89q.com>
- * 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 <http://www.gnu.org/licenses/>.
- */
-
- package com.sk89q.minecraft.util.commands;
-
- import com.sk89q.util.StringUtil;
-
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
- import java.lang.reflect.Modifier;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.logging.Level;
- import java.util.logging.Logger;
-
- /**
- * Manager for handling commands. This allows you to easily process commands,
- * including nested commands, by correctly annotating methods of a class.
- *
- * <p>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
- * {@code 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.</p>
- *
- * <p>Methods of a class to be registered can be static, but if an injector
- * is registered with the class, the instances of the command classes
- * will be created automatically and methods will be called non-statically.</p>
- *
- * <p>To mark a method as a command, use {@link Command}. For nested commands,
- * see {@link NestedCommand}. To handle permissions, use
- * {@link CommandPermissions}.</p>
- *
- * <p>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.</p>
- *
- * @param <T> command sender class
- */
- @SuppressWarnings("ProtectedField")
- public abstract class CommandsManager<T> {
-
- protected static final Logger logger =
- Logger.getLogger(CommandsManager.class.getCanonicalName());
-
- /**
- * 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}. The child map has
- * the key of the command name (one for each alias) with the
- * method.
- */
- protected Map<Method, Map<String, Method>> commands = new HashMap<Method, Map<String, Method>>();
-
- /**
- * Used to store the instances associated with a method.
- */
- protected Map<Method, Object> instances = new HashMap<Method, Object>();
-
- /**
- * Mapping of commands (not including aliases) with a description. This
- * is only for top level commands.
- */
- protected Map<String, String> descs = new HashMap<String, String>();
-
- /**
- * Stores the injector used to getInstance.
- */
- protected Injector injector;
-
- /**
- * Mapping of commands (not including aliases) with a description. This
- * is only for top level commands.
- */
- protected Map<String, String> helpMessages = new HashMap<String, String>();
-
- /**
- * Register an class that contains commands (denoted by {@link Command}.
- * If no dependency injector is specified, then the methods of the
- * class will be registered to be called statically. Otherwise, new
- * instances will be created of the command classes and methods will
- * not be called statically.
- *
- * @param cls the class to register
- */
- public void register(Class<?> cls) {
- registerMethods(cls, null);
- }
-
- /**
- * Register an class that contains commands (denoted by {@link Command}.
- * If no dependency injector is specified, then the methods of the
- * class will be registered to be called statically. Otherwise, new
- * instances will be created of the command classes and methods will
- * not be called statically. A List of {@link Command} annotations from
- * registered commands is returned.
- *
- * @param cls the class to register
- * @return A List of {@link Command} annotations from registered commands,
- * for use in eg. a dynamic command registration system.
- */
- public List<Command> registerAndReturn(Class<?> cls) {
- return registerMethods(cls, null);
- }
-
- /**
- * Register the methods of a class. This will automatically construct
- * instances as necessary.
- *
- * @param cls the class to register
- * @param parent the parent method
- * @return Commands Registered
- */
- public List<Command> registerMethods(Class<?> cls, Method parent) {
- try {
- if (getInjector() == null) {
- return registerMethods(cls, parent, null);
- } else {
- Object obj = getInjector().getInstance(cls);
- return registerMethods(cls, parent, obj);
- }
- } catch (InvocationTargetException e) {
- logger.log(Level.SEVERE, "Failed to register commands", e);
- } catch (IllegalAccessException e) {
- logger.log(Level.SEVERE, "Failed to register commands", e);
- } catch (InstantiationException e) {
- logger.log(Level.SEVERE, "Failed to register commands", e);
- }
- return null;
- }
-
- /**
- * Register the methods of a class.
- *
- * @param cls the class to register
- * @param parent the parent method
- * @param obj the object whose methods will become commands if they are annotated
- * @return a list of commands
- */
- private List<Command> registerMethods(Class<?> cls, Method parent, Object obj) {
- Map<String, Method> map;
- List<Command> registered = new ArrayList<Command>();
-
- // Make a new hash map to cache the commands for this class
- // as looking up methods via reflection is fairly slow
- if (commands.containsKey(parent)) {
- map = commands.get(parent);
- } else {
- map = new HashMap<String, Method>();
- commands.put(parent, map);
- }
-
- for (Method method : cls.getMethods()) {
- if (!method.isAnnotationPresent(Command.class)) {
- continue;
- }
-
- boolean isStatic = Modifier.isStatic(method.getModifiers());
-
- Command cmd = method.getAnnotation(Command.class);
-
- // Cache the aliases too
- for (String alias : cmd.aliases()) {
- map.put(alias, method);
- }
-
- // We want to be able invoke with an instance
- if (!isStatic) {
- // Can't register this command if we don't have an instance
- if (obj == null) {
- continue;
- }
-
- instances.put(method, obj);
- }
-
- // Build a list of commands and their usage details, at least for
- // root level commands
- if (parent == null) {
- final String commandName = cmd.aliases()[0];
- final String desc = cmd.desc();
-
- final String usage = cmd.usage();
- if (usage.isEmpty()) {
- descs.put(commandName, desc);
- } else {
- descs.put(commandName, usage + " - " + desc);
- }
-
- String help = cmd.help();
- if (help.isEmpty()) {
- help = desc;
- }
-
- final CharSequence arguments = getArguments(cmd);
- for (String alias : cmd.aliases()) {
- final String helpMessage = "/" + alias + " " + arguments + "\n\n" + help;
- final String key = alias.replaceAll("/", "");
- String previous = helpMessages.put(key, helpMessage);
-
- if (previous != null && !previous.replaceAll("^/[^ ]+ ", "").equals(helpMessage.replaceAll("^/[^ ]+ ", ""))) {
- helpMessages.put(key, previous + "\n\n" + helpMessage);
- }
- }
-
- }
-
- // Add the command to the registered command list for return
- registered.add(cmd);
-
- // Look for nested commands -- if there are any, those have
- // to be cached too so that they can be quickly looked
- // up when processing commands
- if (method.isAnnotationPresent(NestedCommand.class)) {
- NestedCommand nestedCmd = method.getAnnotation(NestedCommand.class);
-
- for (Class<?> nestedCls : nestedCmd.value()) {
- registerMethods(nestedCls, method);
- }
- }
- }
-
- if (cls.getSuperclass() != null) {
- registerMethods(cls.getSuperclass(), parent, obj);
- }
-
- return registered;
- }
-
- /**
- * Checks to see whether there is a command named such at the root level.
- * This will check aliases as well.
- *
- * @param command the command
- * @return true if the command exists
- */
- public boolean hasCommand(String command) {
- return commands.get(null).containsKey(command.toLowerCase());
- }
-
- /**
- * Get a list of command descriptions. This is only for root commands.
- *
- * @return a map of commands
- */
- public Map<String, String> getCommands() {
- return descs;
- }
-
- /**
- * Get the mapping of methods under a parent command.
- *
- * @return the mapping
- */
- public Map<Method, Map<String, Method>> getMethods() {
- return commands;
- }
-
- /**
- * Get a map from command name to help message. This is only for root commands.
- *
- * @return a map of help messages for each command
- */
- public Map<String, String> getHelpMessages() {
- return helpMessages;
- }
-
- /**
- * Get the usage string for a command.
- *
- * @param args the arguments
- * @param level the depth of the command
- * @param cmd the command annotation
- * @return the usage string
- */
- protected String getUsage(String[] args, int level, Command cmd) {
- final StringBuilder command = new StringBuilder();
-
- command.append('/');
-
- for (int i = 0; i <= level; ++i) {
- command.append(args[i]);
- command.append(' ');
- }
- command.append(getArguments(cmd));
-
- final String help = cmd.help();
- if (!help.isEmpty()) {
- command.append("\n\n");
- command.append(help);
- }
-
- return command.toString();
- }
-
- protected CharSequence getArguments(Command cmd) {
- final String flags = cmd.flags();
-
- final StringBuilder command2 = new StringBuilder();
- if (!flags.isEmpty()) {
- String flagString = flags.replaceAll(".:", "");
- if (!flagString.isEmpty()) {
- command2.append("[-");
- for (int i = 0; i < flagString.length(); ++i) {
- command2.append(flagString.charAt(i));
- }
- command2.append("] ");
- }
- }
-
- command2.append(cmd.usage());
-
- return command2;
- }
-
- /**
- * Get the usage string for a nested command.
- *
- * @param args the arguments
- * @param level the depth of the command
- * @param method the parent method
- * @param player the player
- * @return the usage string
- * @throws CommandException on some error
- */
- protected String getNestedUsage(String[] args, int level, Method method, T player) throws CommandException {
- StringBuilder command = new StringBuilder();
-
- command.append("/");
-
- for (int i = 0; i <= level; ++i) {
- command.append(args[i]).append(" ");
- }
-
- Map<String, Method> map = commands.get(method);
- boolean found = false;
-
- command.append("<");
-
- Set<String> allowedCommands = new HashSet<String>();
-
- for (Map.Entry<String, Method> entry : map.entrySet()) {
- Method childMethod = entry.getValue();
- found = true;
-
- if (hasPermission(childMethod, player)) {
- Command childCmd = childMethod.getAnnotation(Command.class);
-
- allowedCommands.add(childCmd.aliases()[0]);
- }
- }
-
- if (!allowedCommands.isEmpty()) {
- command.append(StringUtil.joinString(allowedCommands, "|", 0));
- } else {
- if (!found) {
- command.append("?");
- } else {
- //command.append("action");
- throw new CommandPermissionsException();
- }
- }
-
- command.append(">");
-
- 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 thrown when the command throws an error
- */
- 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 the arguments
- * @param player the player
- * @param methodArgs the arguments for the method
- * @throws CommandException thrown on command error
- */
- 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);
- }
-
- /**
- * Attempt to execute a command.
- *
- * @param parent the parent method
- * @param args an array of arguments
- * @param player the player
- * @param methodArgs the array of method arguments
- * @param level the depth of the command
- * @throws CommandException thrown on a command error
- */
- public void executeMethod(Method parent, String[] args, T player, Object[] methodArgs, int level) throws CommandException {
- String cmdName = args[level];
-
- Map<String, Method> map = commands.get(parent);
- Method method = map.get(cmdName.toLowerCase());
-
- if (method == null) {
- if (parent == null) { // Root
- throw new UnhandledCommandException();
- } else {
- throw new MissingNestedCommandException("Unknown command: " + cmdName,
- getNestedUsage(args, level - 1, parent, player));
- }
- }
-
- checkPermission(player, method);
-
- int argsCount = args.length - 1 - level;
-
- // checks if we need to execute the body of the nested command method (false)
- // or display the help what commands are available (true)
- // this is all for an args count of 0 if it is > 0 and a NestedCommand Annotation is present
- // it will always handle the methods that NestedCommand points to
- // e.g.:
- // - /cmd - @NestedCommand(executeBody = true) will go into the else loop and execute code in that method
- // - /cmd <arg1> <arg2> - @NestedCommand(executeBody = true) will always go to the nested command class
- // - /cmd <arg1> - @NestedCommand(executeBody = false) will always go to the nested command class not matter the args
- boolean executeNested = method.isAnnotationPresent(NestedCommand.class)
- && (argsCount > 0 || !method.getAnnotation(NestedCommand.class).executeBody());
-
- if (executeNested) {
- if (argsCount == 0) {
- throw new MissingNestedCommandException("Sub-command required.",
- getNestedUsage(args, level, method, player));
- } else {
- executeMethod(method, args, player, methodArgs, level + 1);
- }
- } else if (method.isAnnotationPresent(CommandAlias.class)) {
- CommandAlias aCmd = method.getAnnotation(CommandAlias.class);
- executeMethod(parent, aCmd.value(), player, methodArgs, level);
- } else {
- Command cmd = method.getAnnotation(Command.class);
-
- String[] newArgs = new String[args.length - level];
- System.arraycopy(args, level, newArgs, 0, args.length - level);
-
- final Set<Character> valueFlags = new HashSet<Character>();
-
- char[] flags = cmd.flags().toCharArray();
- Set<Character> newFlags = new HashSet<Character>();
- for (int i = 0; i < flags.length; ++i) {
- if (flags.length > i + 1 && flags[i + 1] == ':') {
- valueFlags.add(flags[i]);
- ++i;
- }
- newFlags.add(flags[i]);
- }
-
- CommandContext context = new CommandContext(newArgs, valueFlags);
-
- if (context.argsLength() < cmd.min()) {
- throw new CommandUsageException("Too few arguments.", getUsage(args, level, cmd));
- }
-
- if (cmd.max() != -1 && context.argsLength() > cmd.max()) {
- throw new CommandUsageException("Too many arguments.", getUsage(args, level, cmd));
- }
-
- if (!cmd.anyFlags()) {
- for (char flag : context.getFlags()) {
- if (!newFlags.contains(flag)) {
- throw new CommandUsageException("Unknown flag: " + flag, getUsage(args, level, cmd));
- }
- }
- }
-
- methodArgs[0] = context;
-
- Object instance = instances.get(method);
-
- invokeMethod(parent, args, player, method, instance, methodArgs, argsCount);
- }
- }
-
- protected void checkPermission(T player, Method method) throws CommandException {
- if (!hasPermission(method, player)) {
- throw new CommandPermissionsException();
- }
- }
-
- public void invokeMethod(Method parent, String[] args, T player, Method method, Object instance, Object[] methodArgs, int level) throws CommandException {
- try {
- method.invoke(instance, methodArgs);
- } catch (IllegalArgumentException e) {
- logger.log(Level.SEVERE, "Failed to execute command", e);
- } catch (IllegalAccessException e) {
- logger.log(Level.SEVERE, "Failed to execute command", e);
- } catch (InvocationTargetException e) {
- if (e.getCause() instanceof CommandException) {
- throw (CommandException) e.getCause();
- }
-
- throw new WrappedCommandException(e.getCause());
- }
- }
-
- /**
- * Returns whether a player has access to a command.
- *
- * @param method the method
- * @param player the player
- * @return true if permission is granted
- */
- protected boolean hasPermission(Method method, T player) {
- CommandPermissions perms = method.getAnnotation(CommandPermissions.class);
- if (perms == null) {
- return true;
- }
-
- for (String perm : perms.value()) {
- if (hasPermission(player, perm)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Returns whether a player permission..
- *
- * @param player the player
- * @param permission the permission
- * @return true if permission is granted
- */
- public abstract boolean hasPermission(T player, String permission);
-
- /**
- * Get the injector used to create new instances. This can be
- * null, in which case only classes will be registered statically.
- *
- * @return an injector instance
- */
- public Injector getInjector() {
- return injector;
- }
-
- /**
- * Set the injector for creating new instances.
- *
- * @param injector injector or null
- */
- public void setInjector(Injector injector) {
- this.injector = injector;
- }
- }