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