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.*; 027import co.aikar.commands.contexts.ContextResolver; 028import co.aikar.commands.contexts.IssuerAwareContextResolver; 029import co.aikar.commands.contexts.IssuerOnlyContextResolver; 030import co.aikar.commands.contexts.OptionalContextResolver; 031import com.google.common.collect.Lists; 032import com.google.common.collect.Maps; 033import com.google.common.collect.Sets; 034import org.jetbrains.annotations.Nullable; 035 036import java.lang.reflect.InvocationTargetException; 037import java.lang.reflect.Method; 038import java.lang.reflect.Parameter; 039import java.util.ArrayList; 040import java.util.Collection; 041import java.util.List; 042import java.util.Map; 043import java.util.Set; 044import java.util.stream.Collectors; 045 046public class RegisteredCommand <R extends CommandExecutionContext<? extends CommandExecutionContext, ? extends CommandIssuer>> { 047 final BaseCommand scope; 048 final String command; 049 final Method method; 050 final String prefSubCommand; 051 final Parameter[] parameters; 052 final ContextResolver<?, R>[] resolvers; 053 final String syntaxText; 054 final String helpText; 055 056 private final String permission; 057 final String complete; 058 final int requiredResolvers; 059 final int optionalResolvers; 060 final List<String> registeredSubcommands = new ArrayList<>(); 061 062 RegisteredCommand(BaseCommand scope, String command, Method method, String prefSubCommand) { 063 this.scope = scope; 064 if ("__unknown".equals(prefSubCommand) || "__default".equals(prefSubCommand)) { 065 prefSubCommand = ""; 066 } 067 this.command = command + (method.getAnnotation(CommandAlias.class) == null && !prefSubCommand.isEmpty() ? prefSubCommand : ""); 068 this.method = method; 069 this.prefSubCommand = prefSubCommand; 070 CommandPermission permissionAnno = method.getAnnotation(CommandPermission.class); 071 this.permission = permissionAnno != null ? scope.manager.getCommandReplacements().replace(permissionAnno.value()) : null; 072 CommandCompletion completionAnno = method.getAnnotation(CommandCompletion.class); 073 this.complete = completionAnno != null ? scope.manager.getCommandReplacements().replace(completionAnno.value()) : null; 074 this.parameters = method.getParameters(); 075 076 Description descriptionAnno = method.getAnnotation(Description.class); 077 this.helpText = descriptionAnno != null ? descriptionAnno.value() : ""; 078 //noinspection unchecked 079 this.resolvers = new ContextResolver[this.parameters.length]; 080 final Syntax syntaxStr = method.getAnnotation(Syntax.class); 081 final CommandManager manager = scope.manager; 082 final CommandContexts commandContexts = manager.getCommandContexts(); 083 084 int requiredResolvers = 0; 085 int optionalResolvers = 0; 086 StringBuilder syntaxB = new StringBuilder(64); 087 088 for (int i = 0; i < parameters.length; i++) { 089 final Parameter parameter = parameters[i]; 090 final Class<?> type = parameter.getType(); 091 092 //noinspection unchecked 093 final ContextResolver<?, R> resolver = commandContexts.getResolver(type); 094 if (resolver != null) { 095 resolvers[i] = resolver; 096 097 if (!scope.manager.isCommandIssuer(type)) { 098 String name = parameter.getName(); 099 if (isOptionalResolver(resolver, parameter)) { 100 optionalResolvers++; 101 if (!(resolver instanceof IssuerOnlyContextResolver)) { 102 syntaxB.append('[').append(name).append("] "); 103 } 104 } else { 105 requiredResolvers++; 106 syntaxB.append('<').append(name).append("> "); 107 } 108 } 109 } else { 110 ACFUtil.sneaky(new InvalidCommandContextException( 111 "Parameter " + type.getSimpleName() + " of " + this.command + " has no applicable context resolver" 112 )); 113 } 114 } 115 String syntaxText = syntaxB.toString(); 116 this.syntaxText = manager.getCommandReplacements().replace(syntaxStr != null ? 117 ACFUtil.replace(syntaxStr.value(), "@syntax", syntaxText) : syntaxText); 118 this.requiredResolvers = requiredResolvers; 119 this.optionalResolvers = optionalResolvers; 120 } 121 122 private boolean isOptionalResolver(ContextResolver<?, R> resolver, Parameter parameter) { 123 return isOptionalResolver(resolver) 124 || parameter.getAnnotation(Optional.class) != null 125 || parameter.getAnnotation(Default.class) != null; 126 } 127 128 private boolean isOptionalResolver(ContextResolver<?, R> resolver) { 129 return resolver instanceof IssuerAwareContextResolver || resolver instanceof IssuerOnlyContextResolver 130 || resolver instanceof OptionalContextResolver; 131 } 132 133 void invoke(CommandIssuer sender, List<String> args) { 134 if (!scope.canExecute(sender, this)) { 135 return; 136 } 137 preCommand(); 138 try { 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 InvalidCommandArgument) { 156 InvalidCommandArgument ica = (InvalidCommandArgument) e; 157 if (ica.key != null) { 158 sender.sendMessage(MessageType.ERROR, ica.key, ica.replacements); 159 } else if (e.getMessage() != null && !e.getMessage().isEmpty()) { 160 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PREFIX, "{message}", e.getMessage()); 161 } 162 if (ica.showSyntax) { 163 scope.showSyntax(sender, this); 164 } 165 } else { 166 boolean handeled = this.scope.manager.handleUncaughtException(scope, this, sender, args, e); 167 if(!handeled){ 168 sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PERFORMING_COMMAND); 169 } 170 this.scope.manager.log(LogLevel.ERROR, "Exception in command: " + command + " " + ACFUtil.join(args), e); 171 } 172 } 173 174 @Nullable 175 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args) throws InvalidCommandArgument { 176 return resolveContexts(sender, args, parameters.length); 177 } 178 @Nullable 179 Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, int argLimit) throws InvalidCommandArgument { 180 args = Lists.newArrayList(args); 181 String[] origArgs = args.toArray(new String[args.size()]); 182 Map<String, Object> passedArgs = Maps.newLinkedHashMap(); 183 int remainingRequired = requiredResolvers; 184 for (int i = 0; i < parameters.length && i < argLimit; i++) { 185 boolean isLast = i == parameters.length - 1; 186 boolean allowOptional = remainingRequired == 0; 187 final Parameter parameter = parameters[i]; 188 final String parameterName = parameter.getName(); 189 final Class<?> type = parameter.getType(); 190 //noinspection unchecked 191 final ContextResolver<?, R> resolver = resolvers[i]; 192 R context = this.scope.manager.createCommandContext(this, parameter, sender, args, i, passedArgs); 193 boolean isOptionalResolver = isOptionalResolver(resolver, parameter); 194 if (!isOptionalResolver) { 195 remainingRequired--; 196 } 197 if (args.isEmpty() && !(isLast && type == String[].class)) { 198 Default def = parameter.getAnnotation(Default.class); 199 Optional opt = parameter.getAnnotation(Optional.class); 200 if (allowOptional && def != null) { 201 args.add(scope.manager.getCommandReplacements().replace(def.value())); 202 } else if (allowOptional && opt != null) { 203 passedArgs.put(parameterName, isOptionalResolver(resolver) ? resolver.getContext(context) : null); 204 //noinspection UnnecessaryContinue 205 continue; 206 } else if (!isOptionalResolver) { 207 scope.showSyntax(sender, this); 208 return null; 209 } 210 } 211 final Values values = parameter.getAnnotation(Values.class); 212 if (values != null) { 213 String arg = !args.isEmpty() ? args.get(0) : ""; 214 215 final String[] split = ACFPatterns.PIPE.split(scope.manager.getCommandReplacements().replace(values.value())); 216 Set<String> possible = Sets.newHashSet(); 217 for (String s : split) { 218 List<String> check = this.scope.manager.getCommandCompletions().getCompletionValues(this, sender, s, origArgs); 219 if (!check.isEmpty()) { 220 possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList())); 221 } else { 222 possible.add(s.toLowerCase()); 223 } 224 } 225 226 if (!possible.contains(arg.toLowerCase())) { 227 throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, 228 "{valid}", ACFUtil.join(possible, ", ")); 229 } 230 } 231 passedArgs.put(parameterName, resolver.getContext(context)); 232 } 233 return passedArgs; 234 } 235 236 boolean hasPermission(CommandIssuer issuer) { 237 return (permission == null || permission.isEmpty() || scope.manager.hasPermission(issuer, permission)) && scope.hasPermission(issuer); 238 } 239 240 public String getPermission() { 241 return permission; 242 } 243 244 public String getPrefSubCommand() { 245 return prefSubCommand; 246 } 247 248 public String getSyntaxText() { 249 return syntaxText; 250 } 251 252 public String getCommand() { 253 return command; 254 } 255 256 public void addSubcommand(String cmd) { 257 this.registeredSubcommands.add(cmd); 258 } 259 public void addSubcommands(Collection<String> cmd) { 260 this.registeredSubcommands.addAll(cmd); 261 } 262}