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}