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.CompletionException; 052import java.util.concurrent.CompletionStage; 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.REPLACEMENTS | Annotations.DEFAULT_EMPTY); 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 CompletionStage<?>) { 154 CompletionStage<?> future = (CompletionStage<?>) 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 || e instanceof InvocationTargetException) { 175 e = e.getCause(); 176 } 177 if (e instanceof ShowCommandHelp) { 178 ShowCommandHelp showHelp = (ShowCommandHelp) e; 179 CommandHelp commandHelp = manager.generateCommandHelp(); 180 if (showHelp.search) { 181 commandHelp.setSearch(showHelp.searchArgs == null ? args : showHelp.searchArgs); 182 } 183 commandHelp.showHelp(sender); 184 } else if (e instanceof InvalidCommandArgument) { 185 InvalidCommandArgument invalidCommandArg = (InvalidCommandArgument) e; 186 if (invalidCommandArg.key != null) { 187 sender.sendMessage(MessageType.ERROR, invalidCommandArg.key, invalidCommandArg.replacements); 188 } else if (e.getMessage() != null && !e.getMessage().isEmpty()) { 189 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PREFIX, "{message}", e.getMessage()); 190 } 191 if (invalidCommandArg.showSyntax) { 192 scope.showSyntax(sender, this); 193 } 194 } else { 195 try { 196 if (!this.manager.handleUncaughtException(scope, this, sender, args, e)) { 197 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PERFORMING_COMMAND); 198 } 199 boolean hasExceptionHandler = this.manager.defaultExceptionHandler != null || this.scope.getExceptionHandler() != null; 200 if (!hasExceptionHandler || this.manager.logUnhandledExceptions) { 201 this.manager.log(LogLevel.ERROR, "Exception in command: " + command + " " + ACFUtil.join(args), e); 202 } 203 } catch (Exception e2) { 204 this.manager.log(LogLevel.ERROR, "Exception in handleException for command: " + command + " " + ACFUtil.join(args), e); 205 this.manager.log(LogLevel.ERROR, "Exception triggered by exception handler:", e2); 206 } 207 } 208 } 209 210 @Nullable 211 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args) throws InvalidCommandArgument { 212 return resolveContexts(sender, args, null); 213 } 214 215 @Nullable 216 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, String name) throws InvalidCommandArgument { 217 args = new ArrayList<>(args); 218 String[] origArgs = args.toArray(new String[args.size()]); 219 Map<String, Object> passedArgs = new LinkedHashMap<>(); 220 int remainingRequired = requiredResolvers; 221 CommandOperationContext opContext = CommandManager.getCurrentCommandOperationContext(); 222 for (int i = 0; i < parameters.length && (name == null || !passedArgs.containsKey(name)); i++) { 223 boolean isLast = i == parameters.length - 1; 224 boolean allowOptional = remainingRequired == 0; 225 final CommandParameter<CEC> parameter = parameters[i]; 226 final String parameterName = parameter.getName(); 227 final Class<?> type = parameter.getType(); 228 final ContextResolver<?, CEC> resolver = parameter.getResolver(); 229 //noinspection unchecked 230 CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, args, i, passedArgs); 231 boolean requiresInput = parameter.requiresInput(); 232 if (requiresInput && remainingRequired > 0) { 233 remainingRequired--; 234 } 235 236 Set<String> parameterPermissions = parameter.getRequiredPermissions(); 237 if (args.isEmpty() && !(isLast && type == String[].class)) { 238 if (allowOptional && parameter.getDefaultValue() != null) { 239 args.add(parameter.getDefaultValue()); 240 } else if (allowOptional && parameter.isOptional()) { 241 Object value; 242 if (!parameter.isOptionalResolver() || !this.manager.hasPermission(sender, parameterPermissions)) { 243 value = null; 244 } else { 245 value = resolver.getContext(context); 246 } 247 248 if (value == null && parameter.getClass().isPrimitive()) { 249 throw new IllegalStateException("Parameter " + parameter.getName() + " is primitive and does not support Optional."); 250 } 251 //noinspection unchecked 252 this.manager.getCommandConditions().validateConditions(context, value); 253 passedArgs.put(parameterName, value); 254 continue; 255 } else if (requiresInput) { 256 scope.showSyntax(sender, this); 257 return null; 258 } 259 } else { 260 if (!this.manager.hasPermission(sender, parameterPermissions)) { 261 sender.sendMessage(MessageType.ERROR, MessageKeys.PERMISSION_DENIED_PARAMETER, "{param}", parameterName); 262 throw new InvalidCommandArgument(false); 263 } 264 } 265 266 if (parameter.getValues() != null) { 267 String arg = !args.isEmpty() ? args.get(0) : ""; 268 269 Set<String> possible = new HashSet<>(); 270 CommandCompletions commandCompletions = this.manager.getCommandCompletions(); 271 for (String s : parameter.getValues()) { 272 if ("*".equals(s) || "@completions".equals(s)) { 273 s = commandCompletions.findDefaultCompletion(this, origArgs); 274 } 275 //noinspection unchecked 276 List<String> check = commandCompletions.getCompletionValues(this, sender, s, origArgs, opContext.isAsync()); 277 if (!check.isEmpty()) { 278 possible.addAll(check.stream().filter(Objects::nonNull). 279 map(String::toLowerCase).collect(Collectors.toList())); 280 } else { 281 possible.add(s.toLowerCase(Locale.ENGLISH)); 282 } 283 } 284 if (!possible.contains(arg.toLowerCase(Locale.ENGLISH))) { 285 throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, 286 "{valid}", ACFUtil.join(possible, ", ")); 287 } 288 } 289 290 Object paramValue = resolver.getContext(context); 291 292 //noinspection unchecked 293 this.manager.getCommandConditions().validateConditions(context, paramValue); 294 passedArgs.put(parameterName, paramValue); 295 } 296 return passedArgs; 297 } 298 299 boolean hasPermission(CommandIssuer issuer) { 300 return this.manager.hasPermission(issuer, getRequiredPermissions()); 301 } 302 303 /** 304 * @see #getRequiredPermissions() 305 * @deprecated 306 */ 307 @Deprecated 308 public String getPermission() { 309 if (this.permission == null || this.permission.isEmpty()) { 310 return null; 311 } 312 return ACFPatterns.COMMA.split(this.permission)[0]; 313 } 314 315 void computePermissions() { 316 this.permissions.clear(); 317 this.permissions.addAll(this.scope.getRequiredPermissions()); 318 if (this.permission != null && !this.permission.isEmpty()) { 319 this.permissions.addAll(Arrays.asList(ACFPatterns.COMMA.split(this.permission))); 320 } 321 } 322 323 public Set<String> getRequiredPermissions() { 324 return this.permissions; 325 } 326 327 public boolean requiresPermission(String permission) { 328 return getRequiredPermissions().contains(permission); 329 } 330 331 public String getPrefSubCommand() { 332 return prefSubCommand; 333 } 334 335 public String getSyntaxText() { 336 return getSyntaxText(null); 337 } 338 339 public String getSyntaxText(CommandIssuer issuer) { 340 if (syntaxText != null) return syntaxText; 341 StringBuilder syntaxBuilder = new StringBuilder(64); 342 for (CommandParameter<?> parameter : parameters) { 343 String syntax = parameter.getSyntax(issuer); 344 if (syntax != null) { 345 if (syntaxBuilder.length() > 0) { 346 syntaxBuilder.append(' '); 347 } 348 syntaxBuilder.append(syntax); 349 } 350 } 351 return syntaxBuilder.toString().trim(); 352 } 353 354 public String getHelpText() { 355 return helpText != null ? helpText : ""; 356 } 357 358 public boolean isPrivate() { 359 return isPrivate; 360 } 361 362 public String getCommand() { 363 return command; 364 } 365 366 public void addSubcommand(String cmd) { 367 this.registeredSubcommands.add(cmd); 368 } 369 370 public void addSubcommands(Collection<String> cmd) { 371 this.registeredSubcommands.addAll(cmd); 372 } 373 374 public <T extends Annotation> T getAnnotation(Class<T> annotation) { 375 return method.getAnnotation(annotation); 376 } 377}