001/* 002 * Copyright (c) 2016-2017 Daniel Ennis (Aikar) - MIT License 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining 005 * a copy of this software and associated documentation files (the 006 * "Software"), to deal in the Software without restriction, including 007 * without limitation the rights to use, copy, modify, merge, publish, 008 * distribute, sublicense, and/or sell copies of the Software, and to 009 * permit persons to whom the Software is furnished to do so, subject to 010 * the following conditions: 011 * 012 * The above copyright notice and this permission notice shall be 013 * included in all copies or substantial portions of the Software. 014 * 015 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 016 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 017 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 018 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 019 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 020 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 021 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 022 */ 023 024package co.aikar.commands; 025 026import co.aikar.commands.annotation.CommandAlias; 027import co.aikar.commands.annotation.CommandCompletion; 028import co.aikar.commands.annotation.CommandPermission; 029import co.aikar.commands.annotation.Conditions; 030import co.aikar.commands.annotation.Description; 031import co.aikar.commands.annotation.HelpSearchTags; 032import co.aikar.commands.annotation.Syntax; 033import co.aikar.commands.contexts.ContextResolver; 034import com.google.common.collect.ImmutableSet; 035import com.google.common.collect.Lists; 036import com.google.common.collect.Maps; 037import com.google.common.collect.Sets; 038import org.jetbrains.annotations.Nullable; 039 040import java.lang.reflect.InvocationTargetException; 041import java.lang.reflect.Method; 042import java.lang.reflect.Parameter; 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.List; 046import java.util.Map; 047import java.util.Set; 048import java.util.stream.Collectors; 049 050@SuppressWarnings("WeakerAccess") 051public class RegisteredCommand <CEC extends CommandExecutionContext<CEC, ? extends CommandIssuer>> { 052 final BaseCommand scope; 053 final Method method; 054 final CommandParameter<CEC>[] parameters; 055 final CommandManager manager; 056 final List<String> registeredSubcommands = new ArrayList<>(); 057 058 String command; 059 String prefSubCommand; 060 String syntaxText; 061 String helpText; 062 String permission; 063 String complete; 064 String conditions; 065 public String helpSearchTags; 066 067 final int requiredResolvers; 068 final int consumeInputResolvers; 069 final int doesNotConsumeInputResolvers; 070 final int optionalResolvers; 071 072 RegisteredCommand(BaseCommand scope, String command, Method method, String prefSubCommand) { 073 this.scope = scope; 074 this.manager = this.scope.manager; 075 final Annotations annotations = this.manager.getAnnotations(); 076 077 if (BaseCommand.CATCHUNKNOWN.equals(prefSubCommand) || BaseCommand.DEFAULT.equals(prefSubCommand)) { 078 prefSubCommand = ""; 079 } 080 this.command = command + (!annotations.hasAnnotation(method, CommandAlias.class, false) && !prefSubCommand.isEmpty() ? prefSubCommand : ""); 081 this.method = method; 082 this.prefSubCommand = prefSubCommand; 083 084 this.permission = annotations.getAnnotationValue(method, CommandPermission.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); 085 this.complete = annotations.getAnnotationValue(method, CommandCompletion.class); 086 this.helpText = annotations.getAnnotationValue(method, Description.class, Annotations.REPLACEMENTS | Annotations.DEFAULT_EMPTY); 087 this.conditions = annotations.getAnnotationValue(method, Conditions.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); 088 this.helpSearchTags = annotations.getAnnotationValue(method, HelpSearchTags.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); 089 090 Parameter[] parameters = method.getParameters(); 091 //noinspection unchecked 092 this.parameters = new CommandParameter[parameters.length]; 093 094 095 int requiredResolvers = 0; 096 int consumeInputResolvers = 0; 097 int doesNotConsumeInputResolvers = 0; 098 int optionalResolvers = 0; 099 StringBuilder syntaxBuilder = new StringBuilder(64); 100 101 for (int i = 0; i < parameters.length; i++) { 102 CommandParameter<CEC> parameter = this.parameters[i] = new CommandParameter<>(this, parameters[i], i); 103 if (!parameter.isCommandIssuer()) { 104 if (!parameter.requiresInput()) { 105 optionalResolvers++; 106 } else { 107 requiredResolvers++; 108 } 109 if (parameter.canConsumeInput()) { 110 consumeInputResolvers++; 111 } else { 112 doesNotConsumeInputResolvers++; 113 } 114 } 115 if (parameter.getSyntax() != null) { 116 if (syntaxBuilder.length() > 0) { 117 syntaxBuilder.append(' '); 118 } 119 syntaxBuilder.append(parameter.getSyntax()); 120 } 121 } 122 String syntaxText = syntaxBuilder.toString().trim(); 123 final String syntaxStr = annotations.getAnnotationValue(method, Syntax.class); 124 this.syntaxText = syntaxStr != null ? ACFUtil.replace(syntaxStr, "@syntax", syntaxText) : syntaxText; 125 this.requiredResolvers = requiredResolvers; 126 this.consumeInputResolvers = consumeInputResolvers; 127 this.doesNotConsumeInputResolvers = doesNotConsumeInputResolvers; 128 this.optionalResolvers = optionalResolvers; 129 } 130 131 132 void invoke(CommandIssuer sender, List<String> args, CommandOperationContext context) { 133 if (!scope.canExecute(sender, this)) { 134 return; 135 } 136 preCommand(); 137 try { 138 this.manager.conditions.validateConditions(context); 139 Map<String, Object> passedArgs = resolveContexts(sender, args); 140 if (passedArgs == null) return; 141 142 method.invoke(scope, passedArgs.values().toArray()); 143 } catch (Exception e) { 144 handleException(sender, args, e); 145 } 146 postCommand(); 147 } 148 public void preCommand() {} 149 public void postCommand() {} 150 151 void handleException(CommandIssuer sender, List<String> args, Exception e) { 152 if (e instanceof InvocationTargetException && e.getCause() instanceof InvalidCommandArgument) { 153 e = (Exception) e.getCause(); 154 } 155 if (e instanceof ShowCommandHelp) { 156 ShowCommandHelp showHelp = (ShowCommandHelp) e; 157 CommandHelp commandHelp = manager.generateCommandHelp(); 158 if (showHelp.search) { 159 commandHelp.setSearch(showHelp.searchArgs == null ? args : showHelp.searchArgs); 160 } 161 commandHelp.showHelp(sender); 162 } else if (e instanceof InvalidCommandArgument) { 163 InvalidCommandArgument invalidCommandArg = (InvalidCommandArgument) e; 164 if (invalidCommandArg.key != null) { 165 sender.sendMessage(MessageType.ERROR, invalidCommandArg.key, invalidCommandArg.replacements); 166 } else if (e.getMessage() != null && !e.getMessage().isEmpty()) { 167 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PREFIX, "{message}", e.getMessage()); 168 } 169 if (invalidCommandArg.showSyntax) { 170 scope.showSyntax(sender, this); 171 } 172 } else { 173 try { 174 if (!this.manager.handleUncaughtException(scope, this, sender, args, e)) { 175 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PERFORMING_COMMAND); 176 } 177 this.manager.log(LogLevel.ERROR, "Exception in command: " + command + " " + ACFUtil.join(args), e); 178 } catch (Exception e2) { 179 this.manager.log(LogLevel.ERROR, "Exception in handleException for command: " + command + " " + ACFUtil.join(args), e); 180 this.manager.log(LogLevel.ERROR, "Exception triggered by exception handler:", e2); 181 } 182 } 183 } 184 185 @Nullable 186 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args) throws InvalidCommandArgument { 187 return resolveContexts(sender, args, parameters.length); 188 } 189 @Nullable 190 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, int argLimit) throws InvalidCommandArgument { 191 args = Lists.newArrayList(args); 192 String[] origArgs = args.toArray(new String[args.size()]); 193 Map<String, Object> passedArgs = Maps.newLinkedHashMap(); 194 int remainingRequired = requiredResolvers; 195 CommandOperationContext opContext = CommandManager.getCurrentCommandOperationContext(); 196 for (int i = 0; i < parameters.length && i < argLimit; i++) { 197 boolean isLast = i == parameters.length - 1; 198 boolean allowOptional = remainingRequired == 0; 199 final CommandParameter<CEC> parameter = parameters[i]; 200 if (parameter.isCommandIssuer()) { 201 argLimit++; 202 } 203 final String parameterName = parameter.getName(); 204 final Class<?> type = parameter.getType(); 205 //noinspection unchecked 206 final ContextResolver<?, CEC> resolver = parameter.getResolver(); 207 //noinspection unchecked 208 CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, args, i, passedArgs); 209 boolean requiresInput = parameter.requiresInput(); 210 if (requiresInput && remainingRequired > 0) { 211 remainingRequired--; 212 } 213 if (args.isEmpty() && !(isLast && type == String[].class)) { 214 if (allowOptional && parameter.getDefaultValue() != null) { 215 args.add(parameter.getDefaultValue()); 216 } else if (allowOptional && parameter.isOptional()) { 217 Object value = parameter.isOptionalResolver() ? resolver.getContext(context) : null; 218 if (value == null && parameter.getClass().isPrimitive()) { 219 throw new IllegalStateException("Parameter " + parameter.getName() + " is primitive and does not support Optional."); 220 } 221 //noinspection unchecked 222 this.manager.conditions.validateConditions(context, value); 223 passedArgs.put(parameterName, value); 224 //noinspection UnnecessaryContinue 225 continue; 226 } else if (requiresInput) { 227 scope.showSyntax(sender, this); 228 return null; 229 } 230 } 231 if (parameter.getValues() != null) { 232 String arg = !args.isEmpty() ? args.get(0) : ""; 233 234 Set<String> possible = Sets.newHashSet(); 235 CommandCompletions commandCompletions = this.manager.getCommandCompletions(); 236 for (String s : parameter.getValues()) { 237 //noinspection unchecked 238 List<String> check = commandCompletions.getCompletionValues(this, sender, s, origArgs, opContext.isAsync()); 239 if (!check.isEmpty()) { 240 possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList())); 241 } else { 242 possible.add(s.toLowerCase()); 243 } 244 } 245 246 if (!possible.contains(arg.toLowerCase())) { 247 throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, 248 "{valid}", ACFUtil.join(possible, ", ")); 249 } 250 } 251 Object paramValue = resolver.getContext(context); 252 //noinspection unchecked 253 this.manager.conditions.validateConditions(context, paramValue); 254 passedArgs.put(parameterName, paramValue); 255 } 256 return passedArgs; 257 } 258 259 boolean hasPermission(CommandIssuer issuer) { 260 return (permission == null || permission.isEmpty() || scope.manager.hasPermission(issuer, permission)) && scope.hasPermission(issuer); 261 } 262 263 264 /** 265 * @see #getRequiredPermissions() 266 * @deprecated 267 */ 268 @Deprecated 269 public String getPermission() { 270 if (this.permission == null || this.permission.isEmpty()) { 271 return null; 272 } 273 return ACFPatterns.COMMA.split(this.permission)[0]; 274 } 275 276 public Set<String> getRequiredPermissions() { 277 if (this.permission == null || this.permission.isEmpty()) { 278 return ImmutableSet.of(); 279 } 280 return Sets.newHashSet(ACFPatterns.COMMA.split(this.permission)); 281 } 282 283 public boolean requiresPermission(String permission) { 284 return getRequiredPermissions().contains(permission) || scope.requiresPermission(permission); 285 } 286 287 public String getPrefSubCommand() { 288 return prefSubCommand; 289 } 290 291 public String getSyntaxText() { 292 return syntaxText; 293 } 294 295 public String getCommand() { 296 return command; 297 } 298 299 public void addSubcommand(String cmd) { 300 this.registeredSubcommands.add(cmd); 301 } 302 public void addSubcommands(Collection<String> cmd) { 303 this.registeredSubcommands.addAll(cmd); 304 } 305}