package net.aufdemrand.denizen.utilities.command; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import net.aufdemrand.denizen.utilities.command.exceptions.*; import net.aufdemrand.denizen.utilities.command.messaging.Messaging; import net.aufdemrand.denizen.utilities.debugging.dB; import net.aufdemrand.denizencore.utilities.CoreUtilities; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; public class CommandManager { private final Map<Class<? extends Annotation>, CommandAnnotationProcessor> annotationProcessors = new HashMap<Class<? extends Annotation>, CommandAnnotationProcessor>(); /* * 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 Method. The child map has the key of the command name * (one for each alias) with the method. */ private final Map<String, Method> commands = new HashMap<String, Method>(); private Injector injector; private final Map<Method, Object> instances = new HashMap<Method, Object>(); private final ListMultimap<Method, Annotation> registeredAnnotations = ArrayListMultimap.create(); private final Set<Method> serverCommands = new HashSet<Method>(); public CommandManager() { registerAnnotationProcessor(new RequirementsProcessor()); } /** * Attempt to execute a command using the root {@link Command} given. A list * of method arguments may be used when calling the command handler method. * <p/> * A command handler method should follow the form * <code>command(CommandContext args, CommandSender sender)</code> where * {@link CommandSender} can be replaced with {@link Player} to only accept * players. The method parameters must include the method args given, if * any. * * @param command The command to execute * @param args The arguments of the command * @param sender The sender of the command * @param methodArgs The method arguments to be used when calling the command * handler * @throws CommandException Any exceptions caused from execution of the command */ public void execute(org.bukkit.command.Command command, String[] args, CommandSender sender, Object... methodArgs) throws CommandException { // must put command into split. String[] newArgs = new String[args.length + 1]; System.arraycopy(args, 0, newArgs, 1, args.length); newArgs[0] = CoreUtilities.toLowerCase(command.getName()); Object[] newMethodArgs = new Object[methodArgs.length + 1]; System.arraycopy(methodArgs, 0, newMethodArgs, 1, methodArgs.length); executeMethod(newArgs, sender, newMethodArgs); } private void executeHelp(String[] args, CommandSender sender) throws CommandException { if (!sender.hasPermission("denizen.basic")) { throw new NoPermissionsException(); } int page = 1; try { page = args.length == 3 ? Integer.parseInt(args[2]) : page; } catch (NumberFormatException e) { sendSpecificHelp(sender, args[0], args[2]); return; } sendHelp(sender, args[0], page); } // Attempt to execute a command. private void executeMethod(String[] args, CommandSender sender, Object[] methodArgs) throws CommandException { String cmdName = CoreUtilities.toLowerCase(args[0]); String modifier = args.length > 1 ? args[1] : ""; boolean help = CoreUtilities.toLowerCase(modifier).equals("help"); Method method = commands.get(cmdName + " " + CoreUtilities.toLowerCase(modifier)); if (method == null && !help) { method = commands.get(cmdName + " *"); } if (method == null && help) { executeHelp(args, sender); return; } if (method == null) { throw new UnhandledCommandException(); } if (!serverCommands.contains(method) && sender instanceof ConsoleCommandSender) { throw new ServerCommandException(); } if (!hasPermission(method, sender)) { throw new NoPermissionsException(); } Command cmd = method.getAnnotation(Command.class); CommandContext context = new CommandContext(sender, args); if (context.argsLength() < cmd.min()) { throw new CommandUsageException("Too few arguments.", getUsage(args, cmd)); } if (cmd.max() != -1 && context.argsLength() > cmd.max()) { throw new CommandUsageException("Too many arguments.", getUsage(args, cmd)); } if (!cmd.flags().contains("*")) { for (char flag : context.getFlags()) { if (cmd.flags().indexOf(String.valueOf(flag)) == -1) { throw new CommandUsageException("Unknown flag: " + flag, getUsage(args, cmd)); } } } methodArgs[0] = context; for (Annotation annotation : registeredAnnotations.get(method)) { CommandAnnotationProcessor processor = annotationProcessors.get(annotation.annotationType()); processor.process(sender, context, annotation, methodArgs); } Object instance = instances.get(method); try { method.invoke(instance, methodArgs); } catch (IllegalArgumentException e) { dB.echoError(e); } catch (IllegalAccessException e) { dB.echoError(e); } catch (InvocationTargetException e) { if (e.getCause() instanceof CommandException) { throw (CommandException) e.getCause(); } throw new WrappedCommandException(e.getCause()); } } /** * A safe version of <code>execute</code> which catches and logs all errors * that occur. Returns whether the command handler should print usage or * not. * * @return Whether further usage should be printed * @see #execute(org.bukkit.command.Command, String[], CommandSender, Object...) */ public boolean executeSafe(org.bukkit.command.Command command, String[] args, CommandSender sender, Object... methodArgs) { try { try { execute(command, args, sender, methodArgs); } catch (ServerCommandException ex) { Messaging.send(sender, "You must be ingame to use that command."); } catch (CommandUsageException ex) { Messaging.sendError(sender, ex.getMessage()); Messaging.sendError(sender, ex.getUsage()); } catch (UnhandledCommandException ex) { return false; } catch (WrappedCommandException ex) { throw ex.getCause(); } catch (CommandException ex) { Messaging.sendError(sender, ex.getMessage()); } catch (NumberFormatException ex) { Messaging.sendError(sender, "That is not a valid number."); } } catch (Throwable ex) { ex.printStackTrace(); if (sender instanceof Player) { Messaging.sendError(sender, "Please report this error: [See console]"); Messaging.sendError(sender, ex.getClass().getName() + ": " + ex.getMessage()); } } return true; } /** * Searches for the closest modifier using Levenshtein distance to the given * top level command and modifier. * * @param command The top level command * @param modifier The modifier to use as the base * @return The closest modifier, or empty */ public String getClosestCommandModifier(String command, String modifier) { int minDist = Integer.MAX_VALUE; command = CoreUtilities.toLowerCase(command); String closest = ""; for (String cmd : commands.keySet()) { String[] split = cmd.split(" "); if (split.length <= 1 || !split[0].equals(command)) { continue; } int distance = getLevenshteinDistance(modifier, split[1]); if (minDist > distance) { minDist = distance; closest = split[1]; } } return closest; } /** * Gets the {@link CommandInfo} for the given top level command and * modifier, or null if not found. * * @param rootCommand The top level command * @param modifier The modifier (may be empty) * @return The command info for the command */ public CommandInfo getCommand(String rootCommand, String modifier) { String joined = rootCommand + ' ' + modifier; for (Map.Entry<String, Method> entry : commands.entrySet()) { if (!entry.getKey().equalsIgnoreCase(joined) || entry.getValue() == null) { continue; } Command commandAnnotation = entry.getValue().getAnnotation(Command.class); if (commandAnnotation == null) { continue; } return new CommandInfo(commandAnnotation); } return null; } /** * Gets all modified and root commands from the given root level command. * For example, if <code>/npc look</code> and <code>/npc jump</code> were * defined, calling <code>getCommands("npc")</code> would return * {@link CommandInfo}s for both commands. * * @param command The root level command * @return The list of {@link CommandInfo}s */ public List<CommandInfo> getCommands(String command) { List<CommandInfo> cmds = new ArrayList<CommandInfo>(); command = CoreUtilities.toLowerCase(command); for (Map.Entry<String, Method> entry : commands.entrySet()) { if (!entry.getKey().startsWith(command) || entry.getValue() == null) { continue; } Command commandAnnotation = entry.getValue().getAnnotation(Command.class); if (commandAnnotation == null) { continue; } cmds.add(new CommandInfo(commandAnnotation)); } return cmds; } private List<String> getLines(CommandSender sender, String baseCommand) { // Ensures that commands with multiple modifiers are only added once Set<CommandInfo> processed = new HashSet<CommandInfo>(); List<String> lines = new ArrayList<String>(); for (CommandInfo info : getCommands(baseCommand)) { Command command = info.getCommandAnnotation(); if (processed.contains(info) || !sender.hasPermission(command.permission())) { continue; } lines.add(format(command, baseCommand)); if (command.modifiers().length > 1) { processed.add(info); } } return lines; } private String getUsage(String[] args, Command cmd) { return "/" + args[0] + " " + cmd.usage(); } /** * Checks to see whether there is a command handler for the given command at * the root level. This will check aliases as well. * * @param cmd The command to check * @param modifier The modifier to check (may be empty) * @return Whether the command is handled */ public boolean hasCommand(org.bukkit.command.Command cmd, String modifier) { String cmdName = CoreUtilities.toLowerCase(cmd.getName()); return commands.containsKey(cmdName + " " + CoreUtilities.toLowerCase(modifier)) || commands.containsKey(cmdName + " *"); } // Returns whether a CommandSender has permission. private boolean hasPermission(CommandSender sender, String perm) { return sender.hasPermission(perm); } // Returns whether a player has access to a command. private boolean hasPermission(Method method, CommandSender sender) { Command cmd = method.getAnnotation(Command.class); return cmd.permission().isEmpty() || hasPermission(sender, cmd.permission()) || hasPermission(sender, "admin"); } /** * Register a class that contains commands (methods annotated with * {@link Command}). If no dependency {@link Injector} is specified, then * only static methods of the class will be registered. Otherwise, new * instances the command class will be created and instance methods will be * called. * * @param clazz The class to scan * @see #setInjector(Injector) */ public void register(Class<?> clazz) { registerMethods(clazz, null); } /** * Registers an {@link CommandAnnotationProcessor} that can process * annotations before a command is executed. * <p/> * Methods with the {@link Command} annotation will have the rest of their * annotations scanned and stored if there is a matching * {@link CommandAnnotationProcessor}. Annotations that do not have a * processor are discarded. The scanning method uses annotations from the * declaring class as a base before narrowing using the method's * annotations. * * @param processor The annotation processor */ public void registerAnnotationProcessor(CommandAnnotationProcessor processor) { annotationProcessors.put(processor.getAnnotationClass(), processor); } /* * Register the methods of a class. This will automatically construct * instances as necessary. */ private void registerMethods(Class<?> clazz, Method parent) { Object obj = injector != null ? injector.getInstance(clazz) : null; registerMethods(clazz, parent, obj); } // Register the methods of a class. private void registerMethods(Class<?> clazz, Method parent, Object obj) { for (Method method : clazz.getMethods()) { if (!method.isAnnotationPresent(Command.class)) { continue; } // We want to be able invoke with an instance if (!Modifier.isStatic(method.getModifiers())) { // Can't register this command if we don't have an instance if (obj == null) { continue; } instances.put(method, obj); } Command cmd = method.getAnnotation(Command.class); // Cache the aliases too for (String alias : cmd.aliases()) { for (String modifier : cmd.modifiers()) { commands.put(alias + " " + modifier, method); } if (!commands.containsKey(alias + " help")) { commands.put(alias + " help", null); } } List<Annotation> annotations = new ArrayList<Annotation>(); for (Annotation annotation : method.getDeclaringClass().getAnnotations()) { Class<? extends Annotation> annotationClass = annotation.annotationType(); if (annotationProcessors.containsKey(annotationClass)) { annotations.add(annotation); } } for (Annotation annotation : method.getAnnotations()) { Class<? extends Annotation> annotationClass = annotation.annotationType(); if (!annotationProcessors.containsKey(annotationClass)) { continue; } Iterator<Annotation> itr = annotations.iterator(); while (itr.hasNext()) { Annotation previous = itr.next(); if (previous.annotationType() == annotationClass) { itr.remove(); } } annotations.add(annotation); } if (annotations.size() > 0) { registeredAnnotations.putAll(method, annotations); } Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length <= 1 || parameterTypes[1] == CommandSender.class) { serverCommands.add(method); } } } private void sendHelp(CommandSender sender, String name, int page) throws CommandException { if (name.equalsIgnoreCase("npc")) { name = "NPC"; } Paginator paginator = new Paginator().header(capitalize(name) + " Help"); for (String line : getLines(sender, CoreUtilities.toLowerCase(name))) { paginator.addLine(line); } if (!paginator.sendPage(sender, page)) { throw new CommandException("The page " + page + " does not exist."); } } private void sendSpecificHelp(CommandSender sender, String rootCommand, String modifier) throws CommandException { CommandInfo info = getCommand(rootCommand, modifier); if (info == null) { throw new CommandException("Command /" + rootCommand + " " + modifier + " not found."); } Messaging.send(sender, format(info.getCommandAnnotation(), rootCommand)); if (info.getCommandAnnotation().help().isEmpty()) { return; } Messaging.send(sender, "<b>" + info.getCommandAnnotation().help()); } public void setInjector(Injector injector) { this.injector = injector; } public static class CommandInfo { private final Command commandAnnotation; public CommandInfo(Command commandAnnotation) { this.commandAnnotation = commandAnnotation; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } CommandInfo other = (CommandInfo) obj; if (commandAnnotation == null) { if (other.commandAnnotation != null) { return false; } } else if (!commandAnnotation.equals(other.commandAnnotation)) { return false; } return true; } public Command getCommandAnnotation() { return commandAnnotation; } @Override public int hashCode() { return 31 + ((commandAnnotation == null) ? 0 : commandAnnotation.hashCode()); } } private static String capitalize(Object string) { String capitalize = string.toString(); return capitalize.length() == 0 ? "" : Character.toUpperCase(capitalize.charAt(0)) + capitalize.substring(1, capitalize.length()); } private static String format(Command command, String alias) { return String.format(COMMAND_FORMAT, alias, (command.usage().isEmpty() ? "" : " " + command.usage()), command.desc()); } private static int getLevenshteinDistance(String s, String t) { if (s == null || t == null) { throw new IllegalArgumentException("Strings must not be null"); } int n = s.length(); // length of s int m = t.length(); // length of t if (n == 0) { return m; } else if (m == 0) { return n; } int p[] = new int[n + 1]; // 'previous' cost array, horizontally int d[] = new int[n + 1]; // cost array, horizontally int _d[]; // placeholder to assist in swapping p and d // indexes into strings s and t int i; // iterates through s int j; // iterates through t char t_j; // jth character of t int cost; // cost for (i = 0; i <= n; i++) { p[i] = i; } for (j = 1; j <= m; j++) { t_j = t.charAt(j - 1); d[0] = j; for (i = 1; i <= n; i++) { cost = s.charAt(i - 1) == t_j ? 0 : 1; // minimum of cell to the left+1, to the top+1, diagonally left // and up +cost d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost); } // copy current distance counts to 'previous row' distance counts _d = p; p = d; d = _d; } // our last action in the above loop was to switch d and p, so p now // actually has the most recent cost counts return p[n]; } private static final String COMMAND_FORMAT = "<7>/<c>%s%s <7>- <e>%s"; }