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.apachecommonslang.ApacheCommonsLangUtil;
028import com.google.common.collect.HashMultimap;
029import com.google.common.collect.ImmutableList;
030import com.google.common.collect.Iterables;
031import com.google.common.collect.Lists;
032import com.google.common.collect.SetMultimap;
033
034import java.lang.reflect.Constructor;
035import java.lang.reflect.InvocationTargetException;
036import java.lang.reflect.Method;
037import java.lang.reflect.Parameter;
038import java.util.*;
039import java.util.Optional;
040import java.util.stream.Collectors;
041import java.util.stream.Stream;
042
043@SuppressWarnings("unused")
044public abstract class BaseCommand {
045
046    public static final String UNKNOWN = "__unknown";
047    public static final String DEFAULT = "__default";
048    final SetMultimap<String, RegisteredCommand> subCommands = HashMultimap.create();
049    private Method preCommandHandler;
050
051    @SuppressWarnings("WeakerAccess")
052    private String execLabel;
053    @SuppressWarnings("WeakerAccess")
054    private String execSubcommand;
055    @SuppressWarnings("WeakerAccess")
056    private String[] origArgs;
057    CommandManager<?, ?, ?> manager = null;
058    BaseCommand parentCommand;
059    Map<String, RootCommand> registeredCommands = new HashMap<>();
060    String description;
061    String commandName;
062    String usageMessage;
063    String permission;
064
065    private ExceptionHandler exceptionHandler = null;
066    CommandOperationContext lastCommandOperationContext;
067
068    public BaseCommand() {}
069    public BaseCommand(String cmd) {
070        this.commandName = cmd;
071    }
072
073    /**
074     * Gets the root command name that the user actually typed
075     * @return Name
076     */
077    public String getExecCommandLabel() {
078        return execLabel;
079    }
080
081    /**
082     * Gets the actual sub command name the user typed
083     * @return Name
084     */
085    public String getExecSubcommand() {
086        return execSubcommand;
087    }
088
089    /**
090     * Gets the actual args in string form the user typed
091     * @return Args
092     */
093    public String[] getOrigArgs() {
094        return origArgs;
095    }
096
097    void setParentCommand(BaseCommand command) {
098        this.parentCommand = command;
099    }
100    void onRegister(CommandManager manager) {
101        onRegister(manager, this.commandName);
102    }
103    void onRegister(CommandManager manager, String cmd) {
104        this.manager = manager;
105        final Class<? extends BaseCommand> self = this.getClass();
106        CommandAlias rootCmdAliasAnno = self.getAnnotation(CommandAlias.class);
107        String rootCmdAlias = rootCmdAliasAnno != null ? manager.getCommandReplacements().replace(rootCmdAliasAnno.value()).toLowerCase() : null;
108        if (cmd == null && rootCmdAlias != null) {
109            cmd = ACFPatterns.PIPE.split(rootCmdAlias)[0];
110        }
111        this.commandName = cmd != null ? cmd : self.getSimpleName().toLowerCase();
112
113        this.description = this.commandName + " commands";
114        this.usageMessage = "/" + this.commandName;
115
116        final CommandPermission perm = self.getAnnotation(CommandPermission.class);
117        if (perm != null) {
118            this.permission = manager.getCommandReplacements().replace(perm.value());
119        }
120
121        boolean foundDefault = false;
122        boolean foundUnknown = false;
123        for (Method method : self.getDeclaredMethods()) {
124            method.setAccessible(true);
125            String sublist = null;
126            String sub = getSubcommandValue(method);
127            final Default def = method.getAnnotation(Default.class);
128            final HelpCommand helpCommand = method.getAnnotation(HelpCommand.class);
129
130            final CommandAlias commandAliases = method.getAnnotation(CommandAlias.class);
131
132            if (def != null || (!foundDefault && helpCommand != null)) {
133                if (!foundDefault) {
134                    registerSubcommand(method, DEFAULT);
135                    if (def != null) {
136                        foundDefault = true;
137                    }
138                } else {
139                    ACFUtil.sneaky(new IllegalStateException("Multiple @Default/@HelpCommand commands, duplicate on " + method.getDeclaringClass().getName() + "#" + method.getName()));
140                }
141            }
142
143            if (sub != null) {
144                sublist = sub;
145            } else if (commandAliases != null) {
146                sublist = commandAliases.value();
147            } else if (helpCommand != null) {
148                sublist = helpCommand.value();
149            }
150
151            UnknownHandler unknown    = method.getAnnotation(UnknownHandler.class);
152            PreCommand     preCommand = method.getAnnotation(PreCommand.class);
153            if (unknown != null || (!foundUnknown && helpCommand != null)) {
154                if (!foundUnknown) {
155                    registerSubcommand(method, UNKNOWN);
156                    if (unknown != null) {
157                        foundUnknown = true;
158                    }
159                } else {
160                    ACFUtil.sneaky(new IllegalStateException("Multiple @UnknownHandler/@HelpCommand commands, duplicate on " + method.getDeclaringClass().getName() + "#" + method.getName()));
161                }
162            } else if (preCommand != null) {
163                if (this.preCommandHandler == null) {
164                    this.preCommandHandler = method;
165                } else {
166                    ACFUtil.sneaky(new IllegalStateException("Multiple @PreCommand commands, duplicate on " + method.getDeclaringClass().getName() + "#" + method.getName()));
167                }
168            }
169            if (sublist != null) {
170                registerSubcommand(method, sublist);
171            }
172        }
173
174        if (rootCmdAlias != null) {
175            Set<String> cmdList = new HashSet<>();
176            Collections.addAll(cmdList, ACFPatterns.PIPE.split(rootCmdAlias));
177            cmdList.remove(cmd);
178            for (String cmdAlias : cmdList) {
179                register(cmdAlias, this);
180            }
181        }
182
183        if (cmd != null) {
184            register(cmd, this);
185        }
186        for (Class<?> clazz : this.getClass().getDeclaredClasses()) {
187            if (BaseCommand.class.isAssignableFrom(clazz)) {
188                try {
189                    BaseCommand subCommand = null;
190                    Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
191                    for (Constructor<?> declaredConstructor : declaredConstructors) {
192
193                        declaredConstructor.setAccessible(true);
194                        Parameter[] parameters = declaredConstructor.getParameters();
195                        if (parameters.length == 1) {
196                            subCommand = (BaseCommand) declaredConstructor.newInstance(this);
197                        } else {
198                            manager.log(LogLevel.INFO, "Found unusable constructor: " + declaredConstructor.getName() + "(" + Stream.of(parameters).map(p -> p.getType().getSimpleName() + " " + p.getName()).collect(Collectors.joining("<c2>,</c2> ")) + ")");
199                        }
200                    }
201                    if (subCommand != null) {
202                        subCommand.setParentCommand(this);
203                        subCommand.onRegister(manager, cmd);
204                        this.subCommands.putAll(subCommand.subCommands);
205                        this.registeredCommands.putAll(subCommand.registeredCommands);
206                    } else {
207                        this.manager.log(LogLevel.ERROR, "Could not find a subcommand ctor for " + clazz.getName());
208                    }
209                } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
210                    e.printStackTrace();
211                }
212            }
213        }
214
215    }
216
217    private String getSubcommandValue(Method method) {
218        final Subcommand sub = method.getAnnotation(Subcommand.class);
219        if (sub == null) {
220            return null;
221        }
222        List<String> subList = new ArrayList<>();
223        subList.add(sub.value());
224        Class<?> clazz = method.getDeclaringClass();
225        while (clazz != null) {
226            Subcommand classSub = clazz.getAnnotation(Subcommand.class);
227            if (classSub != null) {
228                subList.add(classSub.value());
229            }
230            clazz = clazz.getEnclosingClass();
231        }
232        Collections.reverse(subList);
233        return ACFUtil.join(subList, " ");
234    }
235
236    private void register(String name, BaseCommand cmd) {
237        String nameLower = name.toLowerCase();
238        RootCommand rootCommand = manager.obtainRootCommand(nameLower);
239        rootCommand.addChild(cmd);
240
241        this.registeredCommands.put(nameLower, rootCommand);
242    }
243
244    private void registerSubcommand(Method method, String subCommand) {
245        subCommand = manager.getCommandReplacements().replace(subCommand.toLowerCase());
246        final String[] subCommandParts = ACFPatterns.SPACE.split(subCommand);
247        // Must run getSubcommandPossibility BEFORE we rewrite it just after this.
248        Set<String> cmdList = getSubCommandPossibilityList(subCommandParts);
249
250        // Strip pipes off for auto complete addition
251        for (int i = 0; i < subCommandParts.length; i++) {
252            subCommandParts[i] = ACFPatterns.PIPE.split(subCommandParts[i])[0];
253        }
254        String prefSubCommand = ApacheCommonsLangUtil.join(subCommandParts, " ");
255        final CommandAlias cmdAlias = method.getAnnotation(CommandAlias.class);
256
257        final String[] aliasNames = cmdAlias != null ? ACFPatterns.PIPE.split(manager.getCommandReplacements().replace(cmdAlias.value().toLowerCase())) : null;
258        String cmdName = aliasNames != null ? aliasNames[0] : this.commandName + " ";
259        RegisteredCommand cmd = manager.createRegisteredCommand(this, cmdName, method, prefSubCommand);
260
261        for (String subcmd : cmdList) {
262            subCommands.put(subcmd, cmd);
263        }
264        cmd.addSubcommands(cmdList);
265
266        if (aliasNames != null) {
267            for (String name : aliasNames) {
268                register(name, new ForwardingCommand(this, subCommandParts));
269            }
270        }
271    }
272
273    /**
274     * Takes a string like "foo|bar baz|qux" and generates a list of
275     * - foo baz
276     * - foo qux
277     * - bar baz
278     * - bar qux
279     *
280     * For every possible sub command combination
281     *
282     * @param subCommandParts
283     * @return List of all sub command possibilities
284     */
285    private static Set<String> getSubCommandPossibilityList(String[] subCommandParts) {
286        int i = 0;
287        Set<String> current = null;
288        while (true) {
289            Set<String> newList = new HashSet<>();
290
291            if (i < subCommandParts.length) {
292                for (String s1 : ACFPatterns.PIPE.split(subCommandParts[i])) {
293                    if (current != null) {
294                        newList.addAll(current.stream().map(s -> s + " " + s1).collect(Collectors.toList()));
295                    } else {
296                        newList.add(s1);
297                    }
298                }
299            }
300
301            if (i + 1 < subCommandParts.length) {
302                current = newList;
303                i = i + 1;
304                continue;
305            }
306
307            return newList;
308        }
309    }
310
311    public void execute(CommandIssuer issuer, String commandLabel, String[] args) {
312        commandLabel = commandLabel.toLowerCase();
313        try {
314            CommandOperationContext commandContext = preCommandOperation(issuer, commandLabel, args);
315
316            if (args.length > 0) {
317                CommandSearch cmd = findSubCommand(args);
318                if (cmd != null) {
319                    execSubcommand = cmd.getCheckSub();
320                    final String[] execargs = Arrays.copyOfRange(args, cmd.argIndex, args.length);
321                    executeCommand(commandContext, issuer, execargs, cmd.cmd);
322                    return;
323                }
324            }
325
326            if (subCommands.get(DEFAULT) != null) {
327                executeSubcommand(commandContext, DEFAULT, issuer, args);
328                return;
329            }
330
331            if (!executeSubcommand(commandContext, UNKNOWN, issuer, args)) {
332                help(issuer, args);
333            }
334        } finally {
335            postCommandOperation();
336        }
337    }
338
339    private void postCommandOperation() {
340        CommandManager.commandOperationContext.get().pop();
341        execSubcommand = null;
342        execLabel = null;
343        origArgs = new String[]{};
344    }
345
346    private CommandOperationContext preCommandOperation(CommandIssuer issuer, String commandLabel, String[] args) {
347        Stack<CommandOperationContext> contexts = CommandManager.commandOperationContext.get();
348        CommandOperationContext context = this.manager.createCommandOperationContext(this, issuer, commandLabel, args);
349        contexts.push(context);
350        lastCommandOperationContext = context;
351        execSubcommand = null;
352        execLabel = commandLabel;
353        origArgs = args;
354        return context;
355    }
356
357    public CommandIssuer getCurrentCommandIssuer() {
358        return CommandManager.getCurrentCommandIssuer();
359    }
360    public CommandManager getCurrentCommandManager() {
361        return CommandManager.getCurrentCommandManager();
362    }
363
364    private CommandSearch findSubCommand(String[] args) {
365        return findSubCommand(args, false);
366    }
367    private CommandSearch findSubCommand(String[] args, boolean completion) {
368        for (int i = args.length; i >= 0; i--) {
369            String checkSub = ApacheCommonsLangUtil.join(args, " ", 0, i).toLowerCase();
370            Set<RegisteredCommand> cmds = subCommands.get(checkSub);
371
372            final int extraArgs = args.length - i;
373            if (!cmds.isEmpty()) {
374                RegisteredCommand cmd = null;
375                if (cmds.size() == 1) {
376                    cmd = Iterables.getOnlyElement(cmds);
377                } else {
378                    Optional<RegisteredCommand> optCmd = cmds.stream().filter(c -> {
379                        int required = c.requiredResolvers;
380                        int optional = c.optionalResolvers;
381                        return extraArgs <= required + optional && (completion || extraArgs >= required);
382                    }).sorted((c1, c2) -> {
383                        int a = c1.requiredResolvers + c1.optionalResolvers;
384                        int b = c2.requiredResolvers + c2.optionalResolvers;
385
386                        if (a == b) {
387                            return 0;
388                        }
389                        return a < b ? 1 : -1;
390                    }).findFirst();
391                    if (optCmd.isPresent()) {
392                        cmd = optCmd.get();
393                    }
394                }
395                if (cmd != null) {
396                    return new CommandSearch(cmd, i, checkSub);
397                }
398            }
399        }
400        return null;
401    }
402
403    private void executeCommand(CommandOperationContext commandOperationContext,
404                                CommandIssuer issuer, String[] args, RegisteredCommand cmd) {
405        if (cmd.hasPermission(issuer)) {
406            commandOperationContext.setRegisteredCommand(cmd);
407            if (checkPrecommand(commandOperationContext, cmd, issuer, args)) {
408                return;
409            }
410            List<String> sargs = Lists.newArrayList(args);
411            cmd.invoke(issuer, sargs);
412        } else {
413            issuer.sendMessage(MessageType.ERROR, MessageKeys.PERMISSION_DENIED);
414        }
415    }
416
417    public boolean canExecute(CommandIssuer issuer, RegisteredCommand<?> cmd) {
418        return true;
419    }
420
421    public List<String> tabComplete(CommandIssuer issuer, String commandLabel, String[] args)
422        throws IllegalArgumentException {
423
424        commandLabel = commandLabel.toLowerCase();
425        try {
426            CommandOperationContext commandOperationContext = preCommandOperation(issuer, commandLabel, args);
427
428            final CommandSearch search = findSubCommand(args, true);
429
430            String argString = ApacheCommonsLangUtil.join(args, " ").toLowerCase();
431
432            final List<String> cmds = new ArrayList<>();
433
434            if (search != null) {
435                cmds.addAll(completeCommand(commandOperationContext, issuer, search.cmd, Arrays.copyOfRange(args, search.argIndex, args.length), commandLabel));
436            } else if (subCommands.get(UNKNOWN).size() == 1) {
437                cmds.addAll(completeCommand(commandOperationContext, issuer, Iterables.getOnlyElement(subCommands.get(UNKNOWN)), args, commandLabel));
438            }
439
440            for (Map.Entry<String, RegisteredCommand> entry : subCommands.entries()) {
441                final String key = entry.getKey();
442                if (key.startsWith(argString) && !UNKNOWN.equals(key) && !DEFAULT.equals(key)) {
443                    final RegisteredCommand value = entry.getValue();
444                    if (!value.hasPermission(issuer)) {
445                        continue;
446                    }
447                    String prefCommand = value.prefSubCommand;
448
449                    final String[] psplit = ACFPatterns.SPACE.split(prefCommand);
450                    cmds.add(psplit[args.length - 1]);
451                }
452            }
453
454            return filterTabComplete(args[args.length - 1], cmds);
455        } finally {
456            postCommandOperation();
457        }
458    }
459
460    private List<String> completeCommand(CommandOperationContext commandOperationContext, CommandIssuer issuer, RegisteredCommand cmd, String[] args, String commandLabel) {
461        if (!cmd.hasPermission(issuer) || args.length > cmd.requiredResolvers + cmd.optionalResolvers || args.length == 0
462                || cmd.complete == null) {
463            return ImmutableList.of();
464        }
465
466        String[] completions = ACFPatterns.SPACE.split(cmd.complete);
467
468        List<String> cmds = manager.getCommandCompletions().of(commandOperationContext, cmd, issuer, completions, args);
469        return filterTabComplete(args[args.length-1], cmds);
470    }
471
472    private static List<String> filterTabComplete(String arg, List<String> cmds) {
473        return cmds.stream()
474                   .distinct()
475                   .filter(cmd -> cmd != null && (arg.isEmpty() || ApacheCommonsLangUtil.startsWithIgnoreCase(cmd, arg)))
476                   .collect(Collectors.toList());
477    }
478
479
480    private boolean executeSubcommand(CommandOperationContext commandContext, String subcommand, CommandIssuer issuer, String... args) {
481        final Set<RegisteredCommand> defs = subCommands.get(subcommand);
482        RegisteredCommand def = null;
483        if (!defs.isEmpty()) {
484            if (defs.size() == 1) {
485                def = defs.iterator().next();
486            }
487            if (def != null) {
488                executeCommand(commandContext, issuer, args, def);
489                return true;
490            }
491        }
492        return false;
493    }
494
495    private boolean checkPrecommand(CommandOperationContext commandOperationContext, RegisteredCommand cmd, CommandIssuer issuer, String[] args) {
496        Method pre = this.preCommandHandler;
497        if (pre != null) {
498            try {
499                Class<?>[] types = pre.getParameterTypes();
500                Object[] parameters = new Object[pre.getParameterCount()];
501                for (int i = 0; i < parameters.length; i++) {
502                    Class<?> type = types[i];
503                    Object issuerObject = issuer.getIssuer();
504                    if (manager.isCommandIssuer(type) && type.isAssignableFrom(issuerObject.getClass())) {
505                        parameters[i] = issuerObject;
506                    } else if (CommandIssuer.class.isAssignableFrom(type)) {
507                        parameters[i] = issuer;
508                    } else if (RegisteredCommand.class.isAssignableFrom(type)) {
509                        parameters[i] = cmd;
510                    } else if (String[].class.isAssignableFrom((type))) {
511                        parameters[i] = args;
512                    } else {
513                        parameters[i] = null;
514                    }
515                }
516
517                return (boolean) pre.invoke(this, parameters);
518            } catch (IllegalAccessException | InvocationTargetException e) {
519                this.manager.log(LogLevel.ERROR, "Exception encountered while command pre-processing", e);
520            }
521        }
522        return false;
523    }
524
525    /** @deprecated Unstable API */ @Deprecated @UnstableAPI
526    public CommandHelp getCommandHelp() {
527       return manager.generateCommandHelp();
528    }
529
530    /** @deprecated Unstable API */ @Deprecated @UnstableAPI
531    public void showCommandHelp() {
532        getCommandHelp().showHelp();
533    }
534
535    public void help(Object issuer, String[] args) {
536        help(manager.getCommandIssuer(issuer), args);
537    }
538    public void help(CommandIssuer issuer, String[] args) {
539        issuer.sendMessage(MessageType.ERROR, MessageKeys.UNKNOWN_COMMAND);
540    }
541    public void doHelp(Object issuer, String... args) {
542        doHelp(manager.getCommandIssuer(issuer), args);
543    }
544    public void doHelp(CommandIssuer issuer, String... args) {
545        help(issuer, args);
546    }
547
548    public void showSyntax(CommandIssuer issuer, RegisteredCommand<?> cmd) {
549        issuer.sendMessage(MessageType.SYNTAX, MessageKeys.INVALID_SYNTAX,
550                "{command}", "/" + cmd.command,
551                "{syntax}", cmd.syntaxText
552        );
553    }
554
555    public boolean hasPermission(Object issuer) {
556        return hasPermission(manager.getCommandIssuer(issuer));
557    }
558
559    public boolean hasPermission(CommandIssuer issuer) {
560        return permission == null || permission.isEmpty() || (manager.hasPermission(issuer, permission) && (parentCommand == null || parentCommand.hasPermission(issuer)));
561    }
562
563    public String getName() {
564        return commandName;
565    }
566
567    public ExceptionHandler getExceptionHandler() {
568        return exceptionHandler;
569    }
570
571    public BaseCommand setExceptionHandler(ExceptionHandler exceptionHandler) {
572        this.exceptionHandler = exceptionHandler;
573        return this;
574    }
575
576    private static class CommandSearch { RegisteredCommand cmd; int argIndex; String checkSub;
577
578        CommandSearch(RegisteredCommand cmd, int argIndex, String checkSub) {
579            this.cmd = cmd;
580            this.argIndex = argIndex;
581            this.checkSub = checkSub;
582        }
583
584        String getCheckSub() {
585            return this.checkSub;
586        }
587
588        @Override
589        public boolean equals(Object o) {
590            if (this == o) return true;
591            if (o == null || getClass() != o.getClass()) return false;
592            CommandSearch that = (CommandSearch) o;
593            return argIndex == that.argIndex &&
594                    Objects.equals(cmd, that.cmd) &&
595                    Objects.equals(checkSub, that.checkSub);
596        }
597
598        @Override
599        public int hashCode() {
600            return Objects.hash(cmd, argIndex, checkSub);
601        }
602    }
603}