/* * Copyright 2011 ZerothAngel <zerothangel@tyrannyofheaven.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.tyrannyofheaven.bukkit.util.command; import static org.tyrannyofheaven.bukkit.util.ToHLoggingUtils.error; import static org.tyrannyofheaven.bukkit.util.ToHLoggingUtils.warn; import static org.tyrannyofheaven.bukkit.util.ToHMessageUtils.sendMessage; import static org.tyrannyofheaven.bukkit.util.ToHStringUtils.hasText; import static org.tyrannyofheaven.bukkit.util.command.reader.CommandReader.abortBatchProcessing; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; import org.bukkit.plugin.Plugin; import org.tyrannyofheaven.bukkit.util.ToHStringUtils; import org.tyrannyofheaven.bukkit.util.permissions.PermissionException; import org.tyrannyofheaven.bukkit.util.permissions.PermissionUtils; /** * A Bukkit CommandExecutor implementation that ties everything together. * * @author zerothangel */ public class ToHCommandExecutor<T extends Plugin> implements TabExecutor { private final T plugin; private final HandlerExecutor<T> rootHandlerExecutor; private UsageOptions usageOptions = new DefaultUsageOptions(); private final Map<String, TypeCompleter> typeCompleterRegistry = new HashMap<>(); private boolean quoteAware = false; private CommandExceptionHandler exceptionHandler; private String verbosePermissionErrorPermission; /** * Create an instance. * * @param plugin the associated plugin * @param handlers the handler objects */ public ToHCommandExecutor(T plugin, Object... handlers) { if (plugin == null) throw new IllegalArgumentException("plugin cannot be null"); this.plugin = plugin; rootHandlerExecutor = new HandlerExecutor<>(plugin, usageOptions, handlers); // Register default TypeCompleters registerTypeCompleter("constant", new ConstantTypeCompleter()); registerTypeCompleter("player", new PlayerTypeCompleter()); registerTypeCompleter("world", new WorldTypeCompleter()); } /** * Register top-level commands with the server. */ public void registerCommands() { rootHandlerExecutor.registerCommands(this); } public ToHCommandExecutor<T> registerTypeCompleter(String name, TypeCompleter typeCompleter) { if (!ToHStringUtils.hasText(name)) throw new IllegalArgumentException("name must have a value"); if (typeCompleter == null) throw new IllegalArgumentException("typeCompleter cannot be null"); typeCompleterRegistry.put(name, typeCompleter); return this; } public ToHCommandExecutor<T> setUsageOptions(UsageOptions usageOptions) { if (usageOptions == null) throw new IllegalArgumentException("usageOptions cannot be null"); this.usageOptions = usageOptions; return this; } public ToHCommandExecutor<T> setQuoteAware(boolean quoteAware) { this.quoteAware = quoteAware; return this; } public ToHCommandExecutor<T> setExceptionHandler(CommandExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; return this; } public ToHCommandExecutor<T> setVerbosePermissionErrorPermission(String verbosePermissionErrorPermission) { this.verbosePermissionErrorPermission = verbosePermissionErrorPermission; return this; } /* (non-Javadoc) * @see org.bukkit.command.CommandExecutor#onCommand(org.bukkit.command.CommandSender, org.bukkit.command.Command, java.lang.String, java.lang.String[]) */ @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { InvocationChain invChain = new InvocationChain(); try { if (quoteAware) args = split(ToHStringUtils.delimitedString(" ", (Object[])args), true); // NB: We use command.getName() rather than label. This allows the // user to freely add aliases by editing plugin.yml. However, // this also makes aliases in @Command mostly useless. rootHandlerExecutor.execute(sender, command.getName(), label, args, invChain, new CommandSession()); return true; } catch (PermissionException e) { displayPermissionException(sender, e); abortBatchProcessing(); return true; } catch (ParseException e) { // Show message if one was given if (hasText(e.getMessage())) sendMessage(sender, "%s%s", ChatColor.RED, e.getMessage()); if (!invChain.isEmpty()) sendMessage(sender, invChain.getUsageString(usageOptions)); abortBatchProcessing(); return true; } catch (Error e) { // Re-throw Errors throw e; } catch (Throwable t) { if (exceptionHandler != null && exceptionHandler.handleException(sender, command, label, args, t)) { // NB It is up to the CommandExceptionHandler whether or not to call abortBatchProcessing() return true; } sendMessage(sender, ChatColor.RED + "Plugin error; see server log."); error(plugin, "Command handler exception:", t); abortBatchProcessing(); return true; } } @Override public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { if (quoteAware) { // Isolate query argument (last argument) String query; String[] argsNoQuery; if (args.length > 0) { // Have at least one query = args[args.length - 1]; argsNoQuery = Arrays.copyOfRange(args, 0, args.length - 1); } else { query = ""; argsNoQuery = args; } args = split(ToHStringUtils.delimitedString(" ", (Object[])argsNoQuery), false); // Extend and add query args = Arrays.copyOfRange(args, 0, args.length + 1); args[args.length - 1] = query; } try { return rootHandlerExecutor.getTabCompletions(sender, command.getName(), alias, args, null, null, typeCompleterRegistry); } catch (PermissionException e) { displayPermissionException(sender, e); return Collections.emptyList(); } catch (ParseException e) { // Show message if (hasText(e.getMessage())) sendMessage(sender, "%s%s", ChatColor.RED, e.getMessage()); return Collections.emptyList(); } catch (Error e) { throw e; } catch (Throwable t) { warn(plugin, "Tab completion exception:", t); return Collections.emptyList(); } } private void displayPermissionException(CommandSender sender, PermissionException e) { if (verbosePermissionErrorPermission == null || sender.hasPermission(verbosePermissionErrorPermission)) { PermissionUtils.displayPermissionException(sender, e); } else { sendMessage(sender, ChatColor.RED + "You don't have permission to do this."); } } private String[] split(String input, boolean complete) { List<String> result = new ArrayList<>(); SplitState state = SplitState.NORMAL; StringBuilder current = new StringBuilder(); for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); switch (state) { case NORMAL: if (c == '\\') { // Start of escape sequence state = SplitState.ESCAPED; } else if (c == '"') { // Open quotes state = SplitState.QUOTED; } else { if (current.length() == 0) { // Current token empty, skip leading white spaces if (!Character.isWhitespace(c)) current.append(c); } else if (Character.isWhitespace(c)) { // End of token result.add(current.toString()); current = new StringBuilder(); } else { current.append(c); } } break; case ESCAPED: case QUOTED_ESCAPED: if (c == '\\') { current.append('\\'); } else if (c == '"') { current.append('"'); } else { // Not a valid escape current.append('\\'); current.append(c); } state = state == SplitState.ESCAPED ? SplitState.NORMAL : SplitState.QUOTED; break; case QUOTED: if (c == '\\') { state = SplitState.QUOTED_ESCAPED; } else if (c == '"') { // Close quotes state = SplitState.NORMAL; } else { // Append unconditionally current.append(c); } break; default: throw new AssertionError("Unhandled SplitState." + state); } } // Throw if quote isn't terminated. Note we don't really care about unfinished escape sequences. if (complete && (state == SplitState.QUOTED || state == SplitState.QUOTED_ESCAPED)) throw new ParseException("Unterminated quote"); if (state == SplitState.ESCAPED) current.append('\\'); // Check final token if (current.length() > 0) result.add(current.toString()); return result.toArray(new String[result.size()]); } private static enum SplitState { NORMAL, ESCAPED, QUOTED, QUOTED_ESCAPED; } }