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}