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.Syntax;
033import co.aikar.commands.contexts.ContextResolver;
034import com.google.common.collect.ImmutableSet;
035import com.google.common.collect.Lists;
036import com.google.common.collect.Maps;
037import com.google.common.collect.Sets;
038import org.jetbrains.annotations.Nullable;
039
040import java.lang.reflect.InvocationTargetException;
041import java.lang.reflect.Method;
042import java.lang.reflect.Parameter;
043import java.util.ArrayList;
044import java.util.Collection;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048import java.util.stream.Collectors;
049
050@SuppressWarnings("WeakerAccess")
051public class RegisteredCommand <CEC extends CommandExecutionContext<CEC, ? extends CommandIssuer>> {
052    final BaseCommand scope;
053    final Method method;
054    final CommandParameter<CEC>[] parameters;
055    final CommandManager manager;
056    final List<String> registeredSubcommands = new ArrayList<>();
057
058    String command;
059    String prefSubCommand;
060    String syntaxText;
061    String helpText;
062    String permission;
063    String complete;
064    String conditions;
065    public String helpSearchTags;
066
067    final int requiredResolvers;
068    final int consumeInputResolvers;
069    final int doesNotConsumeInputResolvers;
070    final int optionalResolvers;
071
072    RegisteredCommand(BaseCommand scope, String command, Method method, String prefSubCommand) {
073        this.scope = scope;
074        this.manager = this.scope.manager;
075        final Annotations annotations = this.manager.getAnnotations();
076
077        if (BaseCommand.CATCHUNKNOWN.equals(prefSubCommand) || BaseCommand.DEFAULT.equals(prefSubCommand)) {
078            prefSubCommand = "";
079        }
080        this.command = command + (!annotations.hasAnnotation(method, CommandAlias.class, false) && !prefSubCommand.isEmpty() ? prefSubCommand : "");
081        this.method = method;
082        this.prefSubCommand = prefSubCommand;
083
084        this.permission = annotations.getAnnotationValue(method, CommandPermission.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY);
085        this.complete   = annotations.getAnnotationValue(method, CommandCompletion.class);
086        this.helpText = annotations.getAnnotationValue(method, Description.class, Annotations.REPLACEMENTS | Annotations.DEFAULT_EMPTY);
087        this.conditions = annotations.getAnnotationValue(method, Conditions.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY);
088        this.helpSearchTags = annotations.getAnnotationValue(method, HelpSearchTags.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY);
089
090        Parameter[] parameters = method.getParameters();
091        //noinspection unchecked
092        this.parameters = new CommandParameter[parameters.length];
093
094
095        int requiredResolvers = 0;
096        int consumeInputResolvers = 0;
097        int doesNotConsumeInputResolvers = 0;
098        int optionalResolvers = 0;
099        StringBuilder syntaxBuilder = new StringBuilder(64);
100
101        for (int i = 0; i < parameters.length; i++) {
102            CommandParameter<CEC> parameter = this.parameters[i] = new CommandParameter<>(this, parameters[i], i);
103            if (!parameter.isCommandIssuer()) {
104                if (!parameter.requiresInput()) {
105                    optionalResolvers++;
106                } else {
107                    requiredResolvers++;
108                }
109                if (parameter.canConsumeInput()) {
110                    consumeInputResolvers++;
111                } else {
112                    doesNotConsumeInputResolvers++;
113                }
114            }
115            if (parameter.getSyntax() != null) {
116                if (syntaxBuilder.length() > 0) {
117                    syntaxBuilder.append(' ');
118                }
119                syntaxBuilder.append(parameter.getSyntax());
120            }
121        }
122        String syntaxText = syntaxBuilder.toString().trim();
123        final String syntaxStr = annotations.getAnnotationValue(method, Syntax.class);
124        this.syntaxText = syntaxStr != null ? ACFUtil.replace(syntaxStr, "@syntax", syntaxText) : syntaxText;
125        this.requiredResolvers = requiredResolvers;
126        this.consumeInputResolvers = consumeInputResolvers;
127        this.doesNotConsumeInputResolvers = doesNotConsumeInputResolvers;
128        this.optionalResolvers = optionalResolvers;
129    }
130
131
132    void invoke(CommandIssuer sender, List<String> args, CommandOperationContext context) {
133        if (!scope.canExecute(sender, this)) {
134            return;
135        }
136        preCommand();
137        try {
138            this.manager.conditions.validateConditions(context);
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 ShowCommandHelp) {
156            ShowCommandHelp showHelp = (ShowCommandHelp) e;
157            CommandHelp commandHelp = manager.generateCommandHelp();
158            if (showHelp.search) {
159                commandHelp.setSearch(showHelp.searchArgs == null ? args : showHelp.searchArgs);
160            }
161            commandHelp.showHelp(sender);
162        } else if (e instanceof InvalidCommandArgument) {
163            InvalidCommandArgument invalidCommandArg = (InvalidCommandArgument) e;
164            if (invalidCommandArg.key != null) {
165                sender.sendMessage(MessageType.ERROR, invalidCommandArg.key, invalidCommandArg.replacements);
166            } else if (e.getMessage() != null && !e.getMessage().isEmpty()) {
167                sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PREFIX, "{message}", e.getMessage());
168            }
169            if (invalidCommandArg.showSyntax) {
170                scope.showSyntax(sender, this);
171            }
172        } else {
173            try {
174                if (!this.manager.handleUncaughtException(scope, this, sender, args, e)) {
175                    sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PERFORMING_COMMAND);
176                }
177                this.manager.log(LogLevel.ERROR, "Exception in command: " + command + " " + ACFUtil.join(args), e);
178            } catch (Exception e2) {
179                this.manager.log(LogLevel.ERROR, "Exception in handleException for command: " + command + " " + ACFUtil.join(args), e);
180                this.manager.log(LogLevel.ERROR, "Exception triggered by exception handler:", e2);
181            }
182        }
183    }
184
185    @Nullable
186    Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args) throws InvalidCommandArgument {
187        return resolveContexts(sender, args, parameters.length);
188    }
189    @Nullable
190    Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, int argLimit) throws InvalidCommandArgument {
191        args = Lists.newArrayList(args);
192        String[] origArgs = args.toArray(new String[args.size()]);
193        Map<String, Object> passedArgs = Maps.newLinkedHashMap();
194        int remainingRequired = requiredResolvers;
195        CommandOperationContext opContext = CommandManager.getCurrentCommandOperationContext();
196        for (int i = 0; i < parameters.length && i < argLimit; i++) {
197            boolean isLast = i == parameters.length - 1;
198            boolean allowOptional = remainingRequired == 0;
199            final CommandParameter<CEC> parameter = parameters[i];
200            if (parameter.isCommandIssuer()) {
201                argLimit++;
202            }
203            final String parameterName = parameter.getName();
204            final Class<?> type = parameter.getType();
205            //noinspection unchecked
206            final ContextResolver<?, CEC> resolver = parameter.getResolver();
207            //noinspection unchecked
208            CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, args, i, passedArgs);
209            boolean requiresInput = parameter.requiresInput();
210            if (requiresInput && remainingRequired > 0) {
211                remainingRequired--;
212            }
213            if (args.isEmpty() && !(isLast && type == String[].class)) {
214                if (allowOptional && parameter.getDefaultValue() != null) {
215                    args.add(parameter.getDefaultValue());
216                } else if (allowOptional && parameter.isOptional()) {
217                    Object value = parameter.isOptionalResolver() ? resolver.getContext(context) : null;
218                    if (value == null && parameter.getClass().isPrimitive()) {
219                        throw new IllegalStateException("Parameter " + parameter.getName() + " is primitive and does not support Optional.");
220                    }
221                    //noinspection unchecked
222                    this.manager.conditions.validateConditions(context, value);
223                    passedArgs.put(parameterName, value);
224                    //noinspection UnnecessaryContinue
225                    continue;
226                } else if (requiresInput) {
227                    scope.showSyntax(sender, this);
228                    return null;
229                }
230            }
231            if (parameter.getValues() != null) {
232                String arg = !args.isEmpty() ? args.get(0) : "";
233
234                Set<String> possible = Sets.newHashSet();
235                CommandCompletions commandCompletions = this.manager.getCommandCompletions();
236                for (String s : parameter.getValues()) {
237                    //noinspection unchecked
238                    List<String> check = commandCompletions.getCompletionValues(this, sender, s, origArgs, opContext.isAsync());
239                    if (!check.isEmpty()) {
240                        possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList()));
241                    } else {
242                        possible.add(s.toLowerCase());
243                    }
244                }
245
246                if (!possible.contains(arg.toLowerCase())) {
247                    throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF,
248                            "{valid}", ACFUtil.join(possible, ", "));
249                }
250            }
251            Object paramValue = resolver.getContext(context);
252            //noinspection unchecked
253            this.manager.conditions.validateConditions(context, paramValue);
254            passedArgs.put(parameterName, paramValue);
255        }
256        return passedArgs;
257    }
258
259    boolean hasPermission(CommandIssuer issuer) {
260        return (permission == null || permission.isEmpty() || scope.manager.hasPermission(issuer, permission)) && scope.hasPermission(issuer);
261    }
262
263
264    /**
265     * @see #getRequiredPermissions()
266     * @deprecated
267     */
268    @Deprecated
269    public String getPermission() {
270        if (this.permission == null || this.permission.isEmpty()) {
271            return null;
272        }
273        return ACFPatterns.COMMA.split(this.permission)[0];
274    }
275
276    public Set<String> getRequiredPermissions() {
277        if (this.permission == null || this.permission.isEmpty()) {
278            return ImmutableSet.of();
279        }
280        return Sets.newHashSet(ACFPatterns.COMMA.split(this.permission));
281    }
282
283    public boolean requiresPermission(String permission) {
284        return getRequiredPermissions().contains(permission) || scope.requiresPermission(permission);
285    }
286
287    public String getPrefSubCommand() {
288        return prefSubCommand;
289    }
290
291    public String getSyntaxText() {
292        return syntaxText;
293    }
294
295    public String getCommand() {
296        return command;
297    }
298
299    public void addSubcommand(String cmd) {
300        this.registeredSubcommands.add(cmd);
301    }
302    public void addSubcommands(Collection<String> cmd) {
303        this.registeredSubcommands.addAll(cmd);
304    }
305}