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.Private; 033import co.aikar.commands.annotation.Syntax; 034import co.aikar.commands.contexts.ContextResolver; 035import org.jetbrains.annotations.Nullable; 036 037import java.lang.annotation.Annotation; 038import java.lang.reflect.InvocationTargetException; 039import java.lang.reflect.Method; 040import java.lang.reflect.Parameter; 041import java.util.ArrayList; 042import java.util.Arrays; 043import java.util.Collection; 044import java.util.HashSet; 045import java.util.LinkedHashMap; 046import java.util.List; 047import java.util.Locale; 048import java.util.Map; 049import java.util.Set; 050import java.util.concurrent.CompletableFuture; 051import java.util.concurrent.CompletionException; 052import java.util.concurrent.ExecutionException; 053import java.util.stream.Collectors; 054 055@SuppressWarnings("WeakerAccess") 056public class RegisteredCommand<CEC extends CommandExecutionContext<CEC, ? extends CommandIssuer>> { 057 final BaseCommand scope; 058 final Method method; 059 final CommandParameter<CEC>[] parameters; 060 final CommandManager manager; 061 final List<String> registeredSubcommands = new ArrayList<>(); 062 063 String command; 064 String prefSubCommand; 065 String syntaxText; 066 String helpText; 067 String permission; 068 String complete; 069 String conditions; 070 public String helpSearchTags; 071 072 boolean isPrivate; 073 074 final int requiredResolvers; 075 final int consumeInputResolvers; 076 final int doesNotConsumeInputResolvers; 077 final int optionalResolvers; 078 079 final Set<String> permissions = new HashSet<>(); 080 081 RegisteredCommand(BaseCommand scope, String command, Method method, String prefSubCommand) { 082 this.scope = scope; 083 this.manager = this.scope.manager; 084 final Annotations annotations = this.manager.getAnnotations(); 085 086 if (BaseCommand.CATCHUNKNOWN.equals(prefSubCommand) || BaseCommand.DEFAULT.equals(prefSubCommand)) { 087 prefSubCommand = ""; 088 command = command.trim(); 089 } 090 this.command = command + (!annotations.hasAnnotation(method, CommandAlias.class, false) && !prefSubCommand.isEmpty() ? prefSubCommand : ""); 091 this.method = method; 092 this.prefSubCommand = prefSubCommand; 093 094 this.permission = annotations.getAnnotationValue(method, CommandPermission.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); 095 this.complete = annotations.getAnnotationValue(method, CommandCompletion.class, Annotations.DEFAULT_EMPTY); // no replacements as it should be per-issuer 096 this.helpText = annotations.getAnnotationValue(method, Description.class, Annotations.REPLACEMENTS | Annotations.DEFAULT_EMPTY); 097 this.conditions = annotations.getAnnotationValue(method, Conditions.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); 098 this.helpSearchTags = annotations.getAnnotationValue(method, HelpSearchTags.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); 099 100 Parameter[] parameters = method.getParameters(); 101 //noinspection unchecked 102 this.parameters = new CommandParameter[parameters.length]; 103 104 this.isPrivate = annotations.hasAnnotation(method, Private.class) || annotations.getAnnotationFromClass(scope.getClass(), Private.class) != null; 105 106 int requiredResolvers = 0; 107 int consumeInputResolvers = 0; 108 int doesNotConsumeInputResolvers = 0; 109 int optionalResolvers = 0; 110 StringBuilder syntaxBuilder = new StringBuilder(64); 111 112 for (int i = 0; i < parameters.length; i++) { 113 CommandParameter<CEC> parameter = this.parameters[i] = new CommandParameter<>(this, parameters[i], i, i == parameters.length - 1); 114 if (!parameter.isCommandIssuer()) { 115 if (!parameter.requiresInput()) { 116 optionalResolvers++; 117 } else { 118 requiredResolvers++; 119 } 120 if (parameter.canConsumeInput()) { 121 consumeInputResolvers++; 122 } else { 123 doesNotConsumeInputResolvers++; 124 } 125 } 126 if (parameter.getSyntax() != null) { 127 if (syntaxBuilder.length() > 0) { 128 syntaxBuilder.append(' '); 129 } 130 syntaxBuilder.append(parameter.getSyntax()); 131 } 132 } 133 String syntaxText = syntaxBuilder.toString().trim(); 134 final String syntaxStr = annotations.getAnnotationValue(method, Syntax.class); 135 this.syntaxText = syntaxStr != null ? ACFUtil.replace(syntaxStr, "@syntax", syntaxText) : syntaxText; 136 this.requiredResolvers = requiredResolvers; 137 this.consumeInputResolvers = consumeInputResolvers; 138 this.doesNotConsumeInputResolvers = doesNotConsumeInputResolvers; 139 this.optionalResolvers = optionalResolvers; 140 this.computePermissions(); 141 } 142 143 144 void invoke(CommandIssuer sender, List<String> args, CommandOperationContext context) { 145 if (!scope.canExecute(sender, this)) { 146 return; 147 } 148 preCommand(); 149 try { 150 this.manager.getCommandConditions().validateConditions(context); 151 Map<String, Object> passedArgs = resolveContexts(sender, args); 152 if (passedArgs == null) return; 153 154 Object obj = method.invoke(scope, passedArgs.values().toArray()); 155 if (obj instanceof CompletableFuture) { 156 CompletableFuture<?> future = (CompletableFuture) obj; 157 future.exceptionally(t -> { 158 handleException(sender, args, t); 159 return null; 160 }); 161 } 162 } catch (Exception e) { 163 handleException(sender, args, e); 164 } finally { 165 postCommand(); 166 } 167 } 168 169 public void preCommand() { 170 } 171 172 public void postCommand() { 173 } 174 175 void handleException(CommandIssuer sender, List<String> args, Throwable e) { 176 while (e instanceof ExecutionException || e instanceof CompletionException) { 177 e = e.getCause(); 178 } 179 if (e instanceof InvocationTargetException && e.getCause() instanceof InvalidCommandArgument) { 180 e = e.getCause(); 181 } 182 if (e instanceof ShowCommandHelp) { 183 ShowCommandHelp showHelp = (ShowCommandHelp) e; 184 CommandHelp commandHelp = manager.generateCommandHelp(); 185 if (showHelp.search) { 186 commandHelp.setSearch(showHelp.searchArgs == null ? args : showHelp.searchArgs); 187 } 188 commandHelp.showHelp(sender); 189 } else if (e instanceof InvalidCommandArgument) { 190 InvalidCommandArgument invalidCommandArg = (InvalidCommandArgument) e; 191 if (invalidCommandArg.key != null) { 192 sender.sendMessage(MessageType.ERROR, invalidCommandArg.key, invalidCommandArg.replacements); 193 } else if (e.getMessage() != null && !e.getMessage().isEmpty()) { 194 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PREFIX, "{message}", e.getMessage()); 195 } 196 if (invalidCommandArg.showSyntax) { 197 scope.showSyntax(sender, this); 198 } 199 } else { 200 try { 201 if (!this.manager.handleUncaughtException(scope, this, sender, args, e)) { 202 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PERFORMING_COMMAND); 203 } 204 boolean hasExceptionHandler = this.manager.defaultExceptionHandler != null || this.scope.getExceptionHandler() != null; 205 if (!hasExceptionHandler || this.manager.logUnhandledExceptions) { 206 this.manager.log(LogLevel.ERROR, "Exception in command: " + command + " " + ACFUtil.join(args), e); 207 } 208 } catch (Exception e2) { 209 this.manager.log(LogLevel.ERROR, "Exception in handleException for command: " + command + " " + ACFUtil.join(args), e); 210 this.manager.log(LogLevel.ERROR, "Exception triggered by exception handler:", e2); 211 } 212 } 213 } 214 215 @Nullable 216 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args) throws InvalidCommandArgument { 217 return resolveContexts(sender, args, parameters.length); 218 } 219 220 @Nullable 221 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, int argLimit) throws InvalidCommandArgument { 222 args = new ArrayList<>(args); 223 String[] origArgs = args.toArray(new String[args.size()]); 224 Map<String, Object> passedArgs = new LinkedHashMap<>(); 225 int remainingRequired = requiredResolvers; 226 CommandOperationContext opContext = CommandManager.getCurrentCommandOperationContext(); 227 for (int i = 0; i < parameters.length && i < argLimit; i++) { 228 boolean isLast = i == parameters.length - 1; 229 boolean allowOptional = remainingRequired == 0; 230 final CommandParameter<CEC> parameter = parameters[i]; 231 if (!parameter.canConsumeInput()) { 232 argLimit++; 233 } 234 final String parameterName = parameter.getName(); 235 final Class<?> type = parameter.getType(); 236 //noinspection unchecked 237 final ContextResolver<?, CEC> resolver = parameter.getResolver(); 238 //noinspection unchecked 239 CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, args, i, passedArgs); 240 boolean requiresInput = parameter.requiresInput(); 241 if (requiresInput && remainingRequired > 0) { 242 remainingRequired--; 243 } 244 245 Set<String> parameterPermissions = parameter.getRequiredPermissions(); 246 if (args.isEmpty() && !(isLast && type == String[].class)) { 247 if (allowOptional && parameter.getDefaultValue() != null) { 248 args.add(parameter.getDefaultValue()); 249 } else if (allowOptional && parameter.isOptional()) { 250 Object value; 251 if (!parameter.isOptionalResolver() || !this.manager.hasPermission(sender, parameterPermissions)) { 252 value = null; 253 } else { 254 value = resolver.getContext(context); 255 } 256 257 if (value == null && parameter.getClass().isPrimitive()) { 258 throw new IllegalStateException("Parameter " + parameter.getName() + " is primitive and does not support Optional."); 259 } 260 //noinspection unchecked 261 this.manager.getCommandConditions().validateConditions(context, value); 262 passedArgs.put(parameterName, value); 263 continue; 264 } else if (requiresInput) { 265 scope.showSyntax(sender, this); 266 return null; 267 } 268 } else { 269 if (!this.manager.hasPermission(sender, parameterPermissions)) { 270 sender.sendMessage(MessageType.ERROR, MessageKeys.PERMISSION_DENIED_PARAMETER, "{param}", parameterName); 271 throw new InvalidCommandArgument(false); 272 } 273 } 274 275 if (parameter.getValues() != null) { 276 String arg = !args.isEmpty() ? args.get(0) : ""; 277 278 Set<String> possible = new HashSet<>(); 279 CommandCompletions commandCompletions = this.manager.getCommandCompletions(); 280 for (String s : parameter.getValues()) { 281 if ("*".equals(s) || "@completions".equals(s)) { 282 s = commandCompletions.findDefaultCompletion(this, origArgs); 283 } 284 //noinspection unchecked 285 List<String> check = commandCompletions.getCompletionValues(this, sender, s, origArgs, opContext.isAsync()); 286 if (!check.isEmpty()) { 287 possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList())); 288 } else { 289 possible.add(s.toLowerCase(Locale.ENGLISH)); 290 } 291 } 292 if (!possible.contains(arg.toLowerCase(Locale.ENGLISH))) { 293 throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, 294 "{valid}", ACFUtil.join(possible, ", ")); 295 } 296 } 297 298 Object paramValue = resolver.getContext(context); 299 300 //noinspection unchecked 301 this.manager.getCommandConditions().validateConditions(context, paramValue); 302 passedArgs.put(parameterName, paramValue); 303 } 304 return passedArgs; 305 } 306 307 boolean hasPermission(CommandIssuer issuer) { 308 return this.manager.hasPermission(issuer, getRequiredPermissions()); 309 } 310 311 /** 312 * @see #getRequiredPermissions() 313 * @deprecated 314 */ 315 @Deprecated 316 public String getPermission() { 317 if (this.permission == null || this.permission.isEmpty()) { 318 return null; 319 } 320 return ACFPatterns.COMMA.split(this.permission)[0]; 321 } 322 323 void computePermissions() { 324 this.permissions.clear(); 325 this.permissions.addAll(this.scope.getRequiredPermissions()); 326 if (this.permission != null && !this.permission.isEmpty()) { 327 this.permissions.addAll(Arrays.asList(ACFPatterns.COMMA.split(this.permission))); 328 } 329 } 330 331 public Set<String> getRequiredPermissions() { 332 return this.permissions; 333 } 334 335 public boolean requiresPermission(String permission) { 336 return getRequiredPermissions().contains(permission); 337 } 338 339 public String getPrefSubCommand() { 340 return prefSubCommand; 341 } 342 343 public String getSyntaxText() { 344 return syntaxText; 345 } 346 347 public String getHelpText() { 348 return helpText != null ? helpText : ""; 349 } 350 351 public boolean isPrivate() { 352 return isPrivate; 353 } 354 355 public String getCommand() { 356 return command; 357 } 358 359 public void addSubcommand(String cmd) { 360 this.registeredSubcommands.add(cmd); 361 } 362 363 public void addSubcommands(Collection<String> cmd) { 364 this.registeredSubcommands.addAll(cmd); 365 } 366 367 public <T extends Annotation> T getAnnotation(Class<T> annotation) { 368 return method.getAnnotation(annotation); 369 } 370}