package pluginbase.command; import org.jetbrains.annotations.Nullable; import pluginbase.logging.PluginLogger; import pluginbase.messages.BundledMessage; import pluginbase.messages.Message; import pluginbase.messages.Messages; import pluginbase.messages.Theme; import pluginbase.messages.messaging.SendablePluginBaseException; import pluginbase.minecraft.BasePlayer; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; /** * This class is responsible for handling commands. * <p/> * This entails everything from registering them to detecting executed commands and delegating them * to the appropriate command class. * <p/> * This must be implemented fully for a specific Minecraft server implementation. */ public abstract class CommandHandler { protected static final Pattern PATTERN_ON_SPACE = Pattern.compile(" ", Pattern.LITERAL); @NotNull protected final CommandProvider commandProvider; @NotNull protected final Map<String, Class<? extends Command>> registeredCommandClasses; protected final Map<Class<? extends Command>, CommandProvider> commandProviderMap; private final CommandTree commandTree = new CommandTree(); @NotNull private final Map<BasePlayer, QueuedCommand> queuedCommands = new HashMap<BasePlayer, QueuedCommand>(); @NotNull private Map<CommandInfo, String> usageMap = new HashMap<CommandInfo, String>(); /** * Creates a new command handler. * <p/> * Typically you only want one of these per plugin. * * @param commandProvider the provider of commands for this handler. */ protected CommandHandler(@NotNull final CommandProvider commandProvider) { this.commandProvider = commandProvider; this.registeredCommandClasses = new HashMap<String, Class<? extends Command>>(); this.commandProviderMap = new HashMap<Class<? extends Command>, CommandProvider>(); Messages.registerMessages(commandProvider, CommandHandler.class); commandProviderMap.put(DirectoryCommand.class, commandProvider); } /** * Retrieves a PluginLogger for this CommandHandler which is inherited from the plugin passed in during construction. * * @return a PluginLogger for this CommandHandler. */ @NotNull protected PluginLogger getLog() { return commandProvider.getLog(); } //public boolean registerCommmands(String packageName) { //} /** * Registers the command represented by the given command class for the command provider that belongs to this * command handler. * * @param commandClass the command class to register. * @return true if command registered successfully. * @throws IllegalArgumentException if there was some problem with the command class passed in. */ public boolean registerCommand(@NotNull Class<? extends Command> commandClass) throws IllegalArgumentException { return registerCommand(commandProvider, commandClass); } /** * Registers the command represented by the given command class for the given command provider. * <p/> * Generally you can use the single arg version of this method as your commands should typically be using the * same command provider this CommandHandler was initially set up with. * * @param commandProvider the commandProvider to register the command for. This command provider will be used * for instantiation of command instances. * @param commandClass the command class to register. * @return true if command registered successfully. * @throws IllegalArgumentException if there was some problem with the command class passed in. */ public boolean registerCommand(@NotNull CommandProvider commandProvider, @NotNull Class<? extends Command> commandClass) throws IllegalArgumentException { CommandBuilder commandBuilder = new CommandBuilder(commandProvider, commandClass); CommandRegistration commandRegistration = commandBuilder.createCommandRegistration(); assertNotAlreadyRegistered(commandClass, commandRegistration); Command command = commandBuilder.getCommand(); if (register(commandRegistration, command)) { registerRootCommands(commandRegistration); cacheUsageString(commandBuilder); String[] aliases = commandRegistration.getAliases(); for (String alias : aliases) { configureCommandKeys(alias); registeredCommandClasses.put(alias, commandClass); } commandProviderMap.put(commandClass, commandProvider); // Register language in the command class if any. Messages.registerMessages(this.commandProvider, commandClass); getLog().fine("Registered command '%s' to: %s", aliases[0], commandClass); return true; } getLog().severe("Failed to register: " + commandClass); return false; } private static final String SUB_COMMAND_HELP = "Displays a list of sub-commands."; private void registerRootCommands(CommandRegistration commandRegistration) { String[] aliases = commandRegistration.getAliases(); Command command = new DirectoryCommand(commandProvider); for (String alias : aliases) { String[] args = PATTERN_ON_SPACE.split(alias); if (args.length > 1) { List<String> directoryAliases = new ArrayList<String>(args.length - 1); StringBuilder directoryAliasBuilder = new StringBuilder(); for (int i = 0; i < args.length - 1; i++) { if (i != 0) { directoryAliasBuilder.append(" "); } directoryAliasBuilder.append(args[i]); String directoryAlias = directoryAliasBuilder.toString(); if (!registeredCommandClasses.containsKey(directoryAlias)) { directoryAliases.add(directoryAlias); registeredCommandClasses.put(directoryAlias, DirectoryCommand.class); configureCommandKeys(directoryAlias); getLog().finer("Registered directory command '%s'", directoryAlias); } } if (!directoryAliases.isEmpty()) { CommandRegistration directoryCommandRegistration = new CommandRegistration(SUB_COMMAND_HELP, SUB_COMMAND_HELP, directoryAliases.toArray(new String[directoryAliases.size()]), commandProvider); register(directoryCommandRegistration, command); } } } } private void assertNotAlreadyRegistered(Class commandClass, CommandRegistration commandRegistration) { String[] aliases = commandRegistration.getAliases(); for (String alias : aliases) { if (registeredCommandClasses.containsKey(alias)) { throw new IllegalArgumentException("The alias '" + alias + "' for '" + commandClass + "' has already been registered by '" + registeredCommandClasses.get(alias) + "'"); } } } void configureCommandKeys(String primaryAlias) { commandTree.registerKeysForAlias(primaryAlias); } /** * Tells the server implementation to register the given command information as a command so that * someone using the command will delegate the execution to this plugin/command handler. * * @param commandInfo the info for the command to register. * @return true if successfully registered. */ protected abstract boolean register(@NotNull final CommandRegistration commandInfo, @NotNull final Command command); void removedQueuedCommand(@NotNull final BasePlayer player, @NotNull final QueuedCommand command) { if (queuedCommands.containsKey(player) && queuedCommands.get(player).equals(command)) { queuedCommands.remove(player); } } /** Message used when a users tries to confirm a command but has not queued one or the queued one has expired. */ public static final Message NO_QUEUED_COMMANDS = Message.createMessage("commands.queued.none_queued", Theme.SORRY + "Sorry, but you have not used any commands that require confirmation."); /** Default message used when the user must confirm a queued command. */ public static final Message MUST_CONFIRM = Message.createMessage("commands.queued.must_confirm", Theme.DO_THIS + "You must confirm the previous command by typing " + Theme.CMD_HIGHLIGHT + "%s" + "\n" + Theme.INFO + "You have %s to comply."); public static final Message PERMISSION_DENIED = Message.createMessage("commands.permission-denied", Theme.SORRY + "I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error."); /** * Confirms any queued command for the given player. * * @param player the player to confirm queued commands for. * @return true if there was a queued command. */ public boolean confirmCommand(@NotNull final BasePlayer player) { final QueuedCommand queuedCommand = queuedCommands.get(player); if (queuedCommand != null) { queuedCommand.confirm(); return true; } else { return false; } } @Nullable protected final Command getCommand(@NotNull String[] args) { args = commandDetection(args); return _getCommand(args[0]); } @Nullable private Command _getCommand(@NotNull String baseCommandArg) { final Class<? extends Command> commandClass = registeredCommandClasses.get(baseCommandArg); if (commandClass == null) { getLog().severe("Could not locate registered command '" + baseCommandArg + "'"); return null; } final CommandProvider commandProviderForCommand = commandProviderMap.get(commandClass); if (commandProviderForCommand == null) { throw new IllegalStateException("CommandProvider not registered for " + commandClass); } return CommandLoader.loadCommand(commandProviderForCommand, commandClass); } /** * Locates and runs a command executed by a user. * * @param player the user executing the command. * @param args the space separated arguments of the command including the base command itself. * @return true if the command executed successfully. * @throws SendablePluginBaseException if there were any exceptions brought about by the usage of the command. * <p/> * The causes are many fold and include things such as using an improper amount of parameters or attempting to * use a flag not recognized by the command. * TODO This needs to throw an extended PluginBaseException */ public boolean locateAndRunCommand(@NotNull final BasePlayer player, @NotNull String[] args) throws CommandException { args = commandDetection(args); getLog().finest("'%s' is attempting to use command '%s'", player, Arrays.toString(args)); if (this.commandProvider.useQueuedCommands() && !this.registeredCommandClasses.containsKey(this.commandProvider.getCommandPrefix() + "confirm") && args.length == 2 && args[0].equalsIgnoreCase(this.commandProvider.getCommandPrefix()) && args[1].equalsIgnoreCase("confirm")) { getLog().finer("No confirm command registered, using built in confirm..."); if (!confirmCommand(player)) { this.commandProvider.getMessager().message(player, NO_QUEUED_COMMANDS); } return true; } final Command command = _getCommand(args[0]); if (command == null) { return false; } if (command instanceof DirectoryCommand) { ((DirectoryCommand) command).runCommand(player, args[0], commandTree.getTreeAt(args[0])); return true; } if (command.getPerm() != null && !command.getPerm().hasPermission(player)) { BundledMessage permissionMessage = command.getPermissionMessage(); if (permissionMessage == null) { permissionMessage = PERMISSION_DENIED.bundle(); } commandProvider.getMessager().message(player, permissionMessage); return false; } final CommandInfo cmdInfo = command.getClass().getAnnotation(CommandInfo.class); if (cmdInfo == null) { getLog().severe("Missing CommandInfo for command: " + args[0]); return false; } final Set<Character> valueFlags = new HashSet<Character>(); char[] flags = cmdInfo.flags().toCharArray(); final 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]); } final CommandContext context = new CommandContext(args, valueFlags); if (context.argsLength() < cmdInfo.min()) { throw new CommandUsageException(TOO_FEW_ARGUMENTS.bundle(), getUsage(args, 0, command, cmdInfo)); } if (cmdInfo.max() != -1 && context.argsLength() > cmdInfo.max()) { throw new CommandUsageException(TOO_MANY_ARGUMENTS.bundle(), getUsage(args, 0, command, cmdInfo)); } if (!cmdInfo.anyFlags()) { for (char flag : context.getFlags()) { if (!newFlags.contains(flag)) { throw new CommandUsageException(UNKNOWN_FLAG.bundle(flag), getUsage(args, 0, command, cmdInfo)); } } } if (!command.runCommand(player, context)) { throw new CommandUsageException(USAGE_ERROR.bundle(), getUsage(args, 0, command, cmdInfo)); } if (command instanceof QueuedCommand) { final QueuedCommand queuedCommand = (QueuedCommand) command; getLog().finer("Queueing command '%s' for '%s'", queuedCommand, player); queuedCommands.put(player, queuedCommand); final BundledMessage confirmMessage = queuedCommand.getConfirmMessage(); this.commandProvider.getMessager().message(player, confirmMessage); } return true; } public String[] commandDetection(@NotNull final String[] split) { return commandTree.joinArgsForKnownCommands(split); } public static final Message TOO_FEW_ARGUMENTS = Message.createMessage("commands.usage.too_few_arguments", Theme.ERROR + "Too few arguments."); public static final Message TOO_MANY_ARGUMENTS = Message.createMessage("commands.usage.too_many_arguments", Theme.ERROR + "Too many arguments."); public static final Message UNKNOWN_FLAG = Message.createMessage("commands.usage.unknown_flag", Theme.ERROR + "Unknown flag: " + Theme.VALUE + "%s"); public static final Message USAGE_ERROR = Message.createMessage("commands.usage.usage_error", Theme.ERROR + "Usage error..."); public static final Message VALUE_FLAG_ALREADY_GIVEN = Message.createMessage("commands.usage.value_flag_already_given", Theme.ERROR + "Value flag '" + Theme.VALUE + "%s" + Theme.ERROR + "' already given"); public static final Message NO_VALUE_FOR_VALUE_FLAG = Message.createMessage("commands.usage.must_specify_value_for_value_flag", Theme.ERROR + "No value specified for the '" + Theme.VALUE + "-%s" + Theme.ERROR + "' flag."); public static final Message SUB_COMMAND_LIST = Message.createMessage("commands.sub_command_list", Theme.INFO + "The following is a list of sub-commands for '" + Theme.VALUE + "%s" + Theme.INFO + "':\n%s"); /** * Returns a list of strings detailing the usage of the given command. * * @param args * @param level * @param cmd * @param cmdInfo * @return */ protected List<String> getUsage(@NotNull final String[] args, final int level, final Command cmd, @NotNull final CommandInfo cmdInfo) { final List<String> commandUsage = new ArrayList<String>(); final StringBuilder command = new StringBuilder(); command.append(Theme.CMD_USAGE); command.append('/'); for (int i = 0; i <= level; ++i) { command.append(args[i]); command.append(' '); } command.append(getArguments(cmdInfo)); commandUsage.add(command.toString()); final String help; final Message helpMessage = cmd.getHelp(); if (helpMessage != null) { help = commandProvider.getMessager().getLocalizedMessage(helpMessage); } else { help = ""; } if (!help.isEmpty()) { commandUsage.add(help); } return commandUsage; } private void cacheUsageString(CommandBuilder commandBuilder) { usageMap.put(commandBuilder.getCommandInfo(), commandBuilder.getCommandUsageString()); } protected String getArguments(@NotNull final CommandInfo cmdInfo) { return usageMap.containsKey(cmdInfo) ? usageMap.get(cmdInfo) : ""; } protected static class CommandRegistration { private final String[] aliases; private final CommandProvider registeredWith; private final String usage, desc; private final String[] permissions; CommandRegistration(String usage, String desc, String[] aliases, CommandProvider registeredWith) { this(usage, desc, aliases, registeredWith, null); } CommandRegistration(String usage, String desc, String[] aliases, CommandProvider registeredWith, String[] permissions) { this.usage = usage; this.desc = desc; this.aliases = aliases; this.permissions = permissions; this.registeredWith = registeredWith; } public String[] getAliases() { return aliases; } public String getName() { return aliases[0]; } public String getUsage() { return usage; } public String getDesc() { return desc; } public String[] getPermissions() { return permissions; } public CommandProvider getRegisteredWith() { return registeredWith; } } public List<String> tabComplete(@NotNull final BasePlayer player, @NotNull String[] args) { if (args.length > 1) { String[] newArgs = new String[args.length - 1]; System.arraycopy(args, 0, newArgs, 0, args.length - 1); String lastArg = args[args.length - 1]; newArgs = commandDetection(newArgs); args = new String[newArgs.length + 1]; System.arraycopy(newArgs, 0, args, 0, newArgs.length); args[args.length - 1] = lastArg; } final Command command = _getCommand(args[0]); if (command != null) { if (args.length == 2 && command instanceof DirectoryCommand) { return tabCompleteDirectory(player, args); } else if (args.length > 1 && (command.getPerm() == null || command.getPerm().hasPermission(player))) { final CommandInfo cmdInfo = command.getClass().getAnnotation(CommandInfo.class); if (cmdInfo != null) { final Set<Character> valueFlags = new HashSet<Character>(); char[] flags = cmdInfo.flags().toCharArray(); final 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]); } try { CommandContext context = new CommandContext(args, valueFlags); return command.tabComplete(player, context); } catch (CommandException ignore) { } } } } return Collections.emptyList(); } private List<String> tabCompleteDirectory(@NotNull final BasePlayer player, @NotNull String[] args) { CommandTree directoryTree = commandTree.getTreeAt(args[0]); Set<String> subDirectories = directoryTree.getSubDirectories(); Set<String> subCommands = directoryTree.getSubCommands(); Set<String> tabCompleteSet = new HashSet<String>(subDirectories.size() + subCommands.size() + 1); args[1] = args[1].trim().toLowerCase(); for (String subDirectory : subDirectories) { if (subDirectory.startsWith(args[1])) { tabCompleteSet.add(subDirectory); } } List<String> potentialSubCommands = new ArrayList<String>(subCommands.size()); for (String subCommand : subCommands) { if (subCommand.startsWith(args[1])) { potentialSubCommands.add(subCommand); } } for (String potentialSubCommand : potentialSubCommands) { Command subCommand = _getCommand(args[0] + " " + potentialSubCommand); if (subCommand != null && (!player.isPlayer() || (subCommand.getPerm() == null || player.hasPerm(subCommand.getPerm())))) { tabCompleteSet.add(potentialSubCommand); } } if (commandProvider.useQueuedCommands() && !registeredCommandClasses.containsKey(commandProvider.getCommandPrefix() + "confirm") && commandProvider.getCommandPrefix().equalsIgnoreCase(args[0]) && "confirm".startsWith(args[1])) { tabCompleteSet.add("confirm"); } List<String> tabCompleteList = new ArrayList<String>(tabCompleteSet); if (tabCompleteList.size() == 1 && tabCompleteList.get(0).equals(args[1])) { return Collections.emptyList(); } Collections.sort(tabCompleteList); return tabCompleteList; } }