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.*;
027import co.aikar.commands.contexts.ContextResolver;
028import co.aikar.commands.contexts.IssuerAwareContextResolver;
029import co.aikar.commands.contexts.IssuerOnlyContextResolver;
030import co.aikar.commands.contexts.OptionalContextResolver;
031import com.google.common.collect.Lists;
032import com.google.common.collect.Maps;
033import com.google.common.collect.Sets;
034import org.jetbrains.annotations.Nullable;
035
036import java.lang.reflect.InvocationTargetException;
037import java.lang.reflect.Method;
038import java.lang.reflect.Parameter;
039import java.util.ArrayList;
040import java.util.Collection;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044import java.util.stream.Collectors;
045
046public class RegisteredCommand <R extends CommandExecutionContext<? extends CommandExecutionContext, ? extends CommandIssuer>> {
047    final BaseCommand scope;
048    final String command;
049    final Method method;
050    final String prefSubCommand;
051    final Parameter[] parameters;
052    final ContextResolver<?, R>[] resolvers;
053    final String syntaxText;
054    final String helpText;
055
056    private final String permission;
057    final String complete;
058    final int requiredResolvers;
059    final int optionalResolvers;
060    final List<String> registeredSubcommands = new ArrayList<>();
061
062    RegisteredCommand(BaseCommand scope, String command, Method method, String prefSubCommand) {
063        this.scope = scope;
064        if ("__unknown".equals(prefSubCommand) || "__default".equals(prefSubCommand)) {
065            prefSubCommand = "";
066        }
067        this.command = command + (method.getAnnotation(CommandAlias.class) == null && !prefSubCommand.isEmpty() ? prefSubCommand : "");
068        this.method = method;
069        this.prefSubCommand = prefSubCommand;
070        CommandPermission permissionAnno = method.getAnnotation(CommandPermission.class);
071        this.permission = permissionAnno != null ? scope.manager.getCommandReplacements().replace(permissionAnno.value()) : null;
072        CommandCompletion completionAnno = method.getAnnotation(CommandCompletion.class);
073        this.complete = completionAnno != null ? scope.manager.getCommandReplacements().replace(completionAnno.value()) : null;
074        this.parameters = method.getParameters();
075
076        Description descriptionAnno = method.getAnnotation(Description.class);
077        this.helpText = descriptionAnno != null ? descriptionAnno.value() : "";
078        //noinspection unchecked
079        this.resolvers = new ContextResolver[this.parameters.length];
080        final Syntax syntaxStr = method.getAnnotation(Syntax.class);
081        final CommandManager manager = scope.manager;
082        final CommandContexts commandContexts = manager.getCommandContexts();
083
084        int requiredResolvers = 0;
085        int optionalResolvers = 0;
086        StringBuilder syntaxB = new StringBuilder(64);
087
088        for (int i = 0; i < parameters.length; i++) {
089            final Parameter parameter = parameters[i];
090            final Class<?> type = parameter.getType();
091
092            //noinspection unchecked
093            final ContextResolver<?, R> resolver = commandContexts.getResolver(type);
094            if (resolver != null) {
095                resolvers[i] = resolver;
096
097                if (!scope.manager.isCommandIssuer(type)) {
098                    String name = parameter.getName();
099                    if (isOptionalResolver(resolver, parameter)) {
100                        optionalResolvers++;
101                        if (!(resolver instanceof IssuerOnlyContextResolver)) {
102                            syntaxB.append('[').append(name).append("] ");
103                        }
104                    } else {
105                        requiredResolvers++;
106                        syntaxB.append('<').append(name).append("> ");
107                    }
108                }
109            } else {
110                ACFUtil.sneaky(new InvalidCommandContextException(
111                    "Parameter " + type.getSimpleName() + " of " + this.command + " has no applicable context resolver"
112                ));
113            }
114        }
115        String syntaxText = syntaxB.toString();
116        this.syntaxText = manager.getCommandReplacements().replace(syntaxStr != null ?
117                ACFUtil.replace(syntaxStr.value(), "@syntax", syntaxText) : syntaxText);
118        this.requiredResolvers = requiredResolvers;
119        this.optionalResolvers = optionalResolvers;
120    }
121
122    private boolean isOptionalResolver(ContextResolver<?, R> resolver, Parameter parameter) {
123        return isOptionalResolver(resolver)
124                || parameter.getAnnotation(Optional.class) != null
125                || parameter.getAnnotation(Default.class) != null;
126    }
127
128    private boolean isOptionalResolver(ContextResolver<?, R> resolver) {
129        return resolver instanceof IssuerAwareContextResolver || resolver instanceof IssuerOnlyContextResolver
130                || resolver instanceof OptionalContextResolver;
131    }
132
133    void invoke(CommandIssuer sender, List<String> args) {
134        if (!scope.canExecute(sender, this)) {
135            return;
136        }
137        preCommand();
138        try {
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 InvalidCommandArgument) {
156            InvalidCommandArgument ica = (InvalidCommandArgument) e;
157            if (ica.key != null) {
158                sender.sendMessage(MessageType.ERROR, ica.key, ica.replacements);
159            } else if (e.getMessage() != null && !e.getMessage().isEmpty()) {
160                sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PREFIX, "{message}", e.getMessage());
161            }
162            if (ica.showSyntax) {
163                scope.showSyntax(sender, this);
164            }
165        } else {
166            boolean handeled = this.scope.manager.handleUncaughtException(scope, this, sender, args, e);
167            if(!handeled){
168                sender.sendMessage(MessageType.ERROR, MessageKeys.ERROR_PERFORMING_COMMAND);
169            }
170            this.scope.manager.log(LogLevel.ERROR, "Exception in command: " + command + " " + ACFUtil.join(args), e);
171        }
172    }
173
174    @Nullable
175    Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args) throws InvalidCommandArgument {
176        return resolveContexts(sender, args, parameters.length);
177    }
178    @Nullable
179    Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, int argLimit) throws InvalidCommandArgument {
180        args = Lists.newArrayList(args);
181        String[] origArgs = args.toArray(new String[args.size()]);
182        Map<String, Object> passedArgs = Maps.newLinkedHashMap();
183        int remainingRequired = requiredResolvers;
184        for (int i = 0; i < parameters.length && i < argLimit; i++) {
185            boolean isLast = i == parameters.length - 1;
186            boolean allowOptional = remainingRequired == 0;
187            final Parameter parameter = parameters[i];
188            final String parameterName = parameter.getName();
189            final Class<?> type = parameter.getType();
190            //noinspection unchecked
191            final ContextResolver<?, R> resolver = resolvers[i];
192            R context = this.scope.manager.createCommandContext(this, parameter, sender, args, i, passedArgs);
193            boolean isOptionalResolver = isOptionalResolver(resolver, parameter);
194            if (!isOptionalResolver) {
195                remainingRequired--;
196            }
197            if (args.isEmpty() && !(isLast && type == String[].class)) {
198                Default def = parameter.getAnnotation(Default.class);
199                Optional opt = parameter.getAnnotation(Optional.class);
200                if (allowOptional && def != null) {
201                    args.add(scope.manager.getCommandReplacements().replace(def.value()));
202                } else if (allowOptional && opt != null) {
203                    passedArgs.put(parameterName, isOptionalResolver(resolver) ? resolver.getContext(context) : null);
204                    //noinspection UnnecessaryContinue
205                    continue;
206                } else if (!isOptionalResolver) {
207                    scope.showSyntax(sender, this);
208                    return null;
209                }
210            }
211            final Values values = parameter.getAnnotation(Values.class);
212            if (values != null) {
213                String arg = !args.isEmpty() ? args.get(0) : "";
214
215                final String[] split = ACFPatterns.PIPE.split(scope.manager.getCommandReplacements().replace(values.value()));
216                Set<String> possible = Sets.newHashSet();
217                for (String s : split) {
218                    List<String> check = this.scope.manager.getCommandCompletions().getCompletionValues(this, sender, s, origArgs);
219                    if (!check.isEmpty()) {
220                        possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList()));
221                    } else {
222                        possible.add(s.toLowerCase());
223                    }
224                }
225
226                if (!possible.contains(arg.toLowerCase())) {
227                    throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF,
228                            "{valid}", ACFUtil.join(possible, ", "));
229                }
230            }
231            passedArgs.put(parameterName, resolver.getContext(context));
232        }
233        return passedArgs;
234    }
235
236    boolean hasPermission(CommandIssuer issuer) {
237        return (permission == null || permission.isEmpty() || scope.manager.hasPermission(issuer, permission)) && scope.hasPermission(issuer);
238    }
239
240    public String getPermission() {
241        return permission;
242    }
243
244    public String getPrefSubCommand() {
245        return prefSubCommand;
246    }
247
248    public String getSyntaxText() {
249        return syntaxText;
250    }
251
252    public String getCommand() {
253        return command;
254    }
255
256    public void addSubcommand(String cmd) {
257        this.registeredSubcommands.add(cmd);
258    }
259    public void addSubcommands(Collection<String> cmd) {
260        this.registeredSubcommands.addAll(cmd);
261    }
262}