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