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}