package ring.commands; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.logging.Logger; /** * This class provides command handling service to a CommandSender (usually a mobile). * The CommandHandler class maintains a global store of all indexed command objects, as * well as the sender's list of aliased commands. The most important method of this class * is the sendCommand method which returns a CommandResult indicating success or failure * of the given command. * @author projectmoon * */ public final class CommandHandler { // The Command-Sending object this handler is linked to. private CommandSender sender; //Map of all built-in commands. This is shared across //all instances for performance/space reasons. //Doesn't need to be synchronized because Commands are immutable. //TreeMap for guaranteed entry order so that command completion //behavior is always the same. private static Map<String, Command> commands = new TreeMap<String, Command>(); //Map of alternate (aliased) commands. Stored per class instance //So each user can have their own set of aliases. //HashMap for faster alternate cmd lookup. Don't care about order. private HashMap<String, String> alternateCommands; //It's always good to know what's happening! private static final Logger log = Logger.getLogger(CommandHandler.class.getName()); /** * Creates a new CommandHandler with the given CommandSender. * @param sender */ public CommandHandler(CommandSender sender) { this.sender = sender; alternateCommands = new HashMap<String, String>(); // Register the alternate commands. // These commands are checked BEFORE command completion registerAlternateCommand("north", "n"); registerAlternateCommand("south", "s"); registerAlternateCommand("west", "w"); registerAlternateCommand("east", "e"); registerAlternateCommand("up", "u"); registerAlternateCommand("down", "d"); registerAlternateCommand("look", "l"); registerAlternateCommand("look", "lo"); registerAlternateCommand("say", "\'"); registerAlternateCommand("inventory", "inv"); registerAlternateCommand("equipment", "eq"); registerAlternateCommand("prepare", "prep"); registerAlternateCommand("movesilently", "ms"); } /** * Adds a list of Command objects to the set of all * commands. * @param cmds */ public static void addCommands(List<Command> cmds) { for (Command cmd : cmds) { //Command names should never be null. assert (cmd.getCommandName() != null); //If they are null for some reason, ignore it and continue. if (cmd.getCommandName() == null) { log.warning(cmd + " has no command name! Ignoring it."); continue; } if (containsCommand(cmd.getCommandName()) == false) { commands.put(cmd.getCommandName(), cmd); } else { String collision = "Command [" + cmd.getCommandName() +"] is already in the command map!\n" + "Colliding objects: [" + cmd + "] and [" + commands.get(cmd.getCommandName()) + "]"; log.severe(collision); } } } /** * Adds an individual String-Command relation to the command Map. This * method is useful for scripting languages that extend the MUD and * implement their own commands and don't have a CommandIndexer to * populate the command map with. * @param cmd */ public static void addCommand(String cmdKey, Command cmd) { commands.put(cmdKey, cmd); } /** * Tells whether or not the command key is present in the shared command * map. * @param cmdKey * @return true if the key is present, false otherwise. */ public static boolean containsCommand(String cmdKey) { return commands.containsKey(cmdKey); } /** * Finds a Command given a string. This method has three levels of * fallback before giving up. First, it checks the static command list. * If it doesn't find anything there, it checks the local list of alternate * commands. If that doesn't work, it attempts to complete the command. * If none of those work, it returns the "bad" command. * @param cmd * @return The Command object corresponding to the string name, or the Bad command if nothing is found. */ private Command lookup(String cmd) { //First try direct lookup. Command comm = commands.get(cmd); //Next try alternate commands if (comm == null) { String altCmd = alternateCommands.get(cmd); if (altCmd != null) comm = commands.get(altCmd); //Next try command completion. if (comm == null) { comm = completeCommand(cmd); } } if (comm == null) { return new Bad(); } else { return comm; } } /** * Parses a command string. Currently, this just splits up the * command by spaces. * @param command * @return A String array, with each token being a word split on spaces. */ private String[] parseCommandString(String command) { return command.split(" "); } /** * Isolates the parameters of the given parsed command string by * returning a String array that removes the actual command (i.e. parsedCmdString[0]) * from the String array. * @param parsedCmdString * @return A String array containing only the command parameters. */ private String[] isolateParameters(String[] parsedCmdString) { if (parsedCmdString.length == 1) return null; String[] params = new String[parsedCmdString.length - 1]; for (int c = 1; c < parsedCmdString.length; c++) { params[c - 1] = parsedCmdString[c]; } return params; } /** * Sends a command to this CommandHandler. This method assumes that * the CommandSender was the one to send this command. Technically, it * is possible to have an external entity send a command to a any CommandHandler, * but only if they can actually access that handler. The actual execution of the * command is synchronized on the command sender. Some commands may also synchronize * on other objects in order to ensure atomicity. * @param command * @return the CommandResult containing results of the command. */ public void sendCommand(String command) { String[] parsedCmd = parseCommandString(command); //Make sure we have something to parse. if (parsedCmd.length > 0) { Command cmd = lookup(parsedCmd[0]); CommandParameters params = new CommandParameters(isolateParameters(parsedCmd), sender); //actually do the command. handleCommand(cmd, params); } else { //Else there was a space typed... just send a blank //command result. CommandResult res = new CommandResult(); res.send(); } } /** * This method returns a command name based on a fragment given to it. It * returns the first command found in the set. If no suitable command can be * found, it returns null. */ private Command completeCommand(String fragment) { //if the fragment is only 1 letter, it's not something we should bother //looking up; there is TOO much ambiguity. 1 letter commands are also //registered as alternate commands. if (fragment.length() <= 1) { return null; } //if the command is >= 2 letters, we can proceed with completion. //now, loop through all available command names //and see if we can find an available full command for (String key : commands.keySet()) { if (key.startsWith(fragment)) { return commands.get(key); } } return null; } /** * Invokes a Command with the specified parameters. Synchronizes on * command sender. Individual commands may be further synchronized * if necessary. * @param cmd * @param params * @return the result of the command. */ private void handleCommand(Command cmd, CommandParameters params) { synchronized (sender) { try { cmd.execute(sender, params); } catch (RuntimeException e) { log.severe("There was a runtime exception executing the command " + cmd + " for sender " + sender + ":"); e.printStackTrace(); CommandResult cr = new CommandResult(); cr.setFailText("There was an error: " + e.toString()); cr.send(); } } } /** * Registers an alternate command with this CommandHandler. This is used to * implement alias functionality into the MUD, as well as have some built-in shortened * commands that don't require full command lookup (i.e. "n" --> "north"). * @param origCmd the original long form of the command. * @param newCmd the aliased command. * @return true if successful, false otherwise. */ public boolean registerAlternateCommand(String origCmd, String newCmd) { return (alternateCommands.put(newCmd, origCmd) == null); } }