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