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