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.Dependency;
027import co.aikar.locales.MessageKeyProvider;
028import co.aikar.util.Table;
029import org.jetbrains.annotations.NotNull;
030
031import java.lang.reflect.Field;
032import java.lang.reflect.InvocationTargetException;
033import java.lang.reflect.Method;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.IdentityHashMap;
040import java.util.List;
041import java.util.Locale;
042import java.util.Map;
043import java.util.Objects;
044import java.util.Set;
045import java.util.Stack;
046import java.util.UUID;
047import java.util.concurrent.ConcurrentHashMap;
048import java.util.function.Predicate;
049
050
051@SuppressWarnings("WeakerAccess")
052public abstract class CommandManager<
053        IT,
054        I extends CommandIssuer,
055        FT,
056        MF extends MessageFormatter<FT>,
057        CEC extends CommandExecutionContext<CEC, I>,
058        CC extends ConditionContext<I>
059        > {
060
061    /**
062     * This is a stack incase a command calls a command
063     */
064    static ThreadLocal<Stack<CommandOperationContext>> commandOperationContext = ThreadLocal.withInitial(() -> new Stack<CommandOperationContext>() {
065        @Override
066        public synchronized CommandOperationContext peek() {
067            return super.size() == 0 ? null : super.peek();
068        }
069    });
070    protected Map<String, RootCommand> rootCommands = new HashMap<>();
071    protected final CommandReplacements replacements = new CommandReplacements(this);
072    protected final CommandConditions<I, CEC, CC> conditions = new CommandConditions<>(this);
073    protected ExceptionHandler defaultExceptionHandler = null;
074    boolean logUnhandledExceptions = true;
075    protected Table<Class<?>, String, Object> dependencies = new Table<>();
076    protected CommandHelpFormatter helpFormatter = new CommandHelpFormatter(this);
077
078    protected boolean usePerIssuerLocale = false;
079    protected List<IssuerLocaleChangedCallback<I>> localeChangedCallbacks = new ArrayList<>();
080    protected Set<Locale> supportedLanguages = new HashSet<>(Arrays.asList(Locales.ENGLISH, Locales.DUTCH, Locales.GERMAN, Locales.SPANISH, Locales.FRENCH, Locales.CZECH, Locales.PORTUGUESE, Locales.SWEDISH, Locales.NORWEGIAN_BOKMAAL, Locales.NORWEGIAN_NYNORSK, Locales.RUSSIAN, Locales.BULGARIAN, Locales.HUNGARIAN, Locales.TURKISH, Locales.JAPANESE, Locales.CHINESE, Locales.SIMPLIFIED_CHINESE, Locales.TRADITIONAL_CHINESE, Locales.KOREAN));
081    protected Predicate<String> validNamePredicate = name -> true;
082    protected Map<MessageType, MF> formatters = new IdentityHashMap<>();
083    protected MF defaultFormatter;
084    protected int defaultHelpPerPage = 10;
085
086    protected Map<UUID, Locale> issuersLocale = new ConcurrentHashMap<>();
087
088    private Set<String> unstableAPIs = new HashSet<>();
089
090    private Annotations annotations = new Annotations<>(this);
091    private CommandRouter router = new CommandRouter(this);
092
093    public static CommandOperationContext getCurrentCommandOperationContext() {
094        return commandOperationContext.get().peek();
095    }
096
097    public static CommandIssuer getCurrentCommandIssuer() {
098        CommandOperationContext context = commandOperationContext.get().peek();
099        return context != null ? context.getCommandIssuer() : null;
100    }
101
102    public static CommandManager getCurrentCommandManager() {
103        CommandOperationContext context = commandOperationContext.get().peek();
104        return context != null ? context.getCommandManager() : null;
105    }
106
107    public MF setFormat(MessageType type, MF formatter) {
108        return formatters.put(type, formatter);
109    }
110
111    public MF getFormat(MessageType type) {
112        return formatters.getOrDefault(type, defaultFormatter);
113    }
114
115    public void setFormat(MessageType type, FT... colors) {
116        MF format = getFormat(type);
117        for (int i = 1; i <= colors.length; i++) {
118            format.setColor(i, colors[i - 1]);
119        }
120    }
121
122    public void setFormat(MessageType type, int i, FT color) {
123        MF format = getFormat(type);
124        format.setColor(i, color);
125    }
126
127    public MF getDefaultFormatter() {
128        return defaultFormatter;
129    }
130
131    public void setDefaultFormatter(MF defaultFormatter) {
132        this.defaultFormatter = defaultFormatter;
133    }
134
135    public CommandConditions<I, CEC, CC> getCommandConditions() {
136        return conditions;
137    }
138
139    /**
140     * Gets the command contexts manager
141     *
142     * @return Command Contexts
143     */
144    public abstract CommandContexts<?> getCommandContexts();
145
146    /**
147     * Gets the command completions manager
148     *
149     * @return Command Completions
150     */
151    public abstract CommandCompletions<?> getCommandCompletions();
152
153    /**
154     * @deprecated Unstable API
155     */
156    @Deprecated
157    @UnstableAPI
158    public CommandHelp generateCommandHelp(@NotNull String command) {
159        verifyUnstableAPI("help");
160        CommandOperationContext context = getCurrentCommandOperationContext();
161        if (context == null) {
162            throw new IllegalStateException("This method can only be called as part of a command execution.");
163        }
164        return generateCommandHelp(context.getCommandIssuer(), command);
165    }
166
167    /**
168     * @deprecated Unstable API
169     */
170    @Deprecated
171    @UnstableAPI
172    public CommandHelp generateCommandHelp(CommandIssuer issuer, @NotNull String command) {
173        verifyUnstableAPI("help");
174        return generateCommandHelp(issuer, obtainRootCommand(command));
175    }
176
177    /**
178     * @deprecated Unstable API
179     */
180    @Deprecated
181    @UnstableAPI
182    public CommandHelp generateCommandHelp() {
183        verifyUnstableAPI("help");
184        CommandOperationContext context = getCurrentCommandOperationContext();
185        if (context == null) {
186            throw new IllegalStateException("This method can only be called as part of a command execution.");
187        }
188        String commandLabel = context.getCommandLabel();
189        return generateCommandHelp(context.getCommandIssuer(), this.obtainRootCommand(commandLabel));
190    }
191
192    /**
193     * @deprecated Unstable API
194     */
195    @Deprecated
196    @UnstableAPI
197    public CommandHelp generateCommandHelp(CommandIssuer issuer, RootCommand rootCommand) {
198        verifyUnstableAPI("help");
199        return new CommandHelp(this, rootCommand, issuer);
200    }
201
202    /**
203     * @deprecated Unstable API
204     */
205    @Deprecated
206    @UnstableAPI
207    public int getDefaultHelpPerPage() {
208        verifyUnstableAPI("help");
209        return defaultHelpPerPage;
210    }
211
212    /**
213     * @deprecated Unstable API
214     */
215    @Deprecated
216    @UnstableAPI
217    public void setDefaultHelpPerPage(int defaultHelpPerPage) {
218        verifyUnstableAPI("help");
219        this.defaultHelpPerPage = defaultHelpPerPage;
220    }
221
222    /**
223     * @deprecated Unstable API
224     */
225    @Deprecated
226    @UnstableAPI
227    public void setHelpFormatter(CommandHelpFormatter helpFormatter) {
228        this.helpFormatter = helpFormatter;
229    }
230
231    /**
232     * @deprecated Unstable API
233     */
234    @Deprecated
235    @UnstableAPI
236    public CommandHelpFormatter getHelpFormatter() {
237        return helpFormatter;
238    }
239
240    CommandRouter getRouter() {
241        return router;
242    }
243
244    /**
245     * Registers a command with ACF
246     *
247     * @param command The command to register
248     * @return boolean
249     */
250    public abstract void registerCommand(BaseCommand command);
251
252    public abstract boolean hasRegisteredCommands();
253
254    public abstract boolean isCommandIssuer(Class<?> type);
255
256    // TODO: Change this to IT if we make a breaking change
257    public abstract I getCommandIssuer(Object issuer);
258
259    public abstract RootCommand createRootCommand(String cmd);
260
261    /**
262     * Returns a Locales Manager to add and modify language tables for your commands.
263     *
264     * @return
265     */
266    public abstract Locales getLocales();
267
268    public boolean usingPerIssuerLocale() {
269        return usePerIssuerLocale;
270    }
271
272    public boolean usePerIssuerLocale(boolean setting) {
273        boolean old = usePerIssuerLocale;
274        usePerIssuerLocale = setting;
275        return old;
276    }
277
278    public boolean isValidName(@NotNull String name) {
279        return validNamePredicate.test(name);
280    }
281
282    public @NotNull Predicate<String> getValidNamePredicate() {
283        return validNamePredicate;
284    }
285
286    public void setValidNamePredicate(@NotNull Predicate<String> isValidName) {
287        this.validNamePredicate = isValidName;
288    }
289
290    public ConditionContext createConditionContext(CommandIssuer issuer, String config) {
291        //noinspection unchecked
292        return new ConditionContext(issuer, config);
293    }
294
295    public abstract CommandExecutionContext createCommandContext(RegisteredCommand command, CommandParameter parameter, CommandIssuer sender, List<String> args, int i, Map<String, Object> passedArgs);
296
297    public abstract CommandCompletionContext createCompletionContext(RegisteredCommand command, CommandIssuer sender, String input, String config, String[] args);
298
299    public abstract void log(final LogLevel level, final String message, final Throwable throwable);
300
301    public void log(final LogLevel level, final String message) {
302        log(level, message, null);
303    }
304
305    /**
306     * Lets you add custom string replacements that can be applied to annotation values,
307     * to reduce duplication/repetition of common values such as permission nodes and command prefixes.
308     * <p>
309     * Any replacement registered starts with a %
310     * <p>
311     * So for ex @CommandPermission("%staff")
312     *
313     * @return Replacements Manager
314     */
315    public CommandReplacements getCommandReplacements() {
316        return replacements;
317    }
318
319    public boolean hasPermission(CommandIssuer issuer, Set<String> permissions) {
320        for (String permission : permissions) {
321            if (!hasPermission(issuer, permission)) {
322                return false;
323            }
324        }
325        return true;
326    }
327
328    public boolean hasPermission(CommandIssuer issuer, String permission) {
329        if (permission == null || permission.isEmpty()) {
330            return true;
331        }
332        for (String perm : ACFPatterns.COMMA.split(permission)) {
333            if (!perm.isEmpty() && !issuer.hasPermission(perm)) {
334                return false;
335            }
336        }
337        return true;
338    }
339
340    public synchronized RootCommand getRootCommand(@NotNull String cmd) {
341        return rootCommands.get(ACFPatterns.SPACE.split(cmd.toLowerCase(Locale.ENGLISH), 2)[0]);
342    }
343
344    public synchronized RootCommand obtainRootCommand(@NotNull String cmd) {
345        return rootCommands.computeIfAbsent(ACFPatterns.SPACE.split(cmd.toLowerCase(Locale.ENGLISH), 2)[0], this::createRootCommand);
346    }
347
348    public abstract Collection<RootCommand> getRegisteredRootCommands();
349
350    public RegisteredCommand createRegisteredCommand(BaseCommand command, String cmdName, Method method, String prefSubCommand) {
351        return new RegisteredCommand(command, cmdName, method, prefSubCommand);
352    }
353
354    /**
355     * Sets the default {@link ExceptionHandler} that is called when an exception occurs while executing a command, if the command doesn't have its own exception handler registered.
356     *
357     * @param exceptionHandler the handler that should handle uncaught exceptions.  May not be null if logExceptions is false
358     */
359    public void setDefaultExceptionHandler(ExceptionHandler exceptionHandler) {
360        if (exceptionHandler == null && !this.logUnhandledExceptions) {
361            throw new IllegalArgumentException("You may not disable the default exception handler and have logging of unhandled exceptions disabled");
362        }
363        defaultExceptionHandler = exceptionHandler;
364    }
365
366    /**
367     * Sets the default {@link ExceptionHandler} that is called when an exception occurs while executing a command, if the command doesn't have its own exception handler registered, and lets you control if ACF should also log the exception still.
368     * <p>
369     * If you disable logging, you need to log it yourself in your handler.
370     *
371     * @param exceptionHandler the handler that should handle uncaught exceptions. May not be null if logExceptions is false
372     * @param logExceptions    Whether or not to log exceptions.
373     */
374    public void setDefaultExceptionHandler(ExceptionHandler exceptionHandler, boolean logExceptions) {
375        if (exceptionHandler == null && !logExceptions) {
376            throw new IllegalArgumentException("You may not disable the default exception handler and have logging of unhandled exceptions disabled");
377        }
378        this.logUnhandledExceptions = logExceptions;
379        this.defaultExceptionHandler = exceptionHandler;
380    }
381
382    public boolean isLoggingUnhandledExceptions() {
383        return this.logUnhandledExceptions;
384    }
385
386    /**
387     * Gets the current default exception handler, might be null.
388     *
389     * @return the default exception handler
390     */
391    public ExceptionHandler getDefaultExceptionHandler() {
392        return defaultExceptionHandler;
393    }
394
395    protected boolean handleUncaughtException(BaseCommand scope, RegisteredCommand registeredCommand, CommandIssuer sender, List<String> args, Throwable t) {
396        if (t instanceof InvocationTargetException && t.getCause() != null) {
397            t = t.getCause();
398        }
399        boolean result = false;
400        if (scope.getExceptionHandler() != null) {
401            result = scope.getExceptionHandler().execute(scope, registeredCommand, sender, args, t);
402        } else if (defaultExceptionHandler != null) {
403            result = defaultExceptionHandler.execute(scope, registeredCommand, sender, args, t);
404        }
405        return result;
406    }
407
408    public void sendMessage(IT issuerArg, MessageType type, MessageKeyProvider key, String... replacements) {
409        sendMessage(getCommandIssuer(issuerArg), type, key, replacements);
410    }
411
412    public void sendMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) {
413        String message = formatMessage(issuer, type, key, replacements);
414
415        for (String msg : ACFPatterns.NEWLINE.split(message)) {
416            issuer.sendMessageInternal(ACFUtil.rtrim(msg));
417        }
418    }
419
420    public String formatMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) {
421        String message = getLocales().getMessage(issuer, key.getMessageKey());
422        if (replacements.length > 0) {
423            message = ACFUtil.replaceStrings(message, replacements);
424        }
425
426        message = getCommandReplacements().replace(message);
427        message = getLocales().replaceI18NStrings(message);
428
429        MessageFormatter formatter = formatters.getOrDefault(type, defaultFormatter);
430        if (formatter != null) {
431            message = formatter.format(message);
432        }
433        return message;
434    }
435
436    public void onLocaleChange(IssuerLocaleChangedCallback<I> onChange) {
437        localeChangedCallbacks.add(onChange);
438    }
439
440    public void notifyLocaleChange(I issuer, Locale oldLocale, Locale newLocale) {
441        localeChangedCallbacks.forEach(cb -> {
442            try {
443                cb.onIssuerLocaleChange(issuer, oldLocale, newLocale);
444            } catch (Exception e) {
445                this.log(LogLevel.ERROR, "Error in notifyLocaleChange", e);
446            }
447        });
448    }
449
450    public Locale setIssuerLocale(IT issuer, Locale locale) {
451        I commandIssuer = getCommandIssuer(issuer);
452
453        Locale old = issuersLocale.put(commandIssuer.getUniqueId(), locale);
454        if (!Objects.equals(old, locale)) {
455            this.notifyLocaleChange(commandIssuer, old, locale);
456        }
457
458        return old;
459    }
460
461    public Locale getIssuerLocale(CommandIssuer issuer) {
462        if (usingPerIssuerLocale() && issuer != null) {
463            Locale locale = issuersLocale.get(issuer.getUniqueId());
464            if (locale != null) {
465                return locale;
466            }
467        }
468
469        return getLocales().getDefaultLocale();
470    }
471
472    CommandOperationContext<I> createCommandOperationContext(BaseCommand command, CommandIssuer issuer, String commandLabel, String[] args, boolean isAsync) {
473        //noinspection unchecked
474        return new CommandOperationContext<>(
475                this,
476                (I) issuer,
477                command,
478                commandLabel,
479                args,
480                isAsync
481        );
482    }
483
484    /**
485     * Gets a list of all currently supported languages for this manager.
486     * These locales will be automatically loaded from
487     *
488     * @return
489     */
490    public Set<Locale> getSupportedLanguages() {
491        return supportedLanguages;
492    }
493
494    /**
495     * Adds a new locale to the list of automatic Locales to load Message Bundles for.
496     * All bundles loaded under the previous supported languages will now automatically load for this new locale too.
497     *
498     * @param locale
499     */
500    public void addSupportedLanguage(Locale locale) {
501        supportedLanguages.add(locale);
502        getLocales().loadMissingBundles();
503    }
504
505    /**
506     * Registers an instance of a class to be registered as an injectable dependency.<br>
507     * The command manager will attempt to inject all fields in a command class that are annotated with
508     * {@link co.aikar.commands.annotation.Dependency} with the provided instance.
509     *
510     * @param clazz    the class the injector should look for when injecting
511     * @param instance the instance of the class that should be injected
512     * @throws IllegalStateException when there is already an instance for the provided class registered
513     */
514    public <T> void registerDependency(Class<? extends T> clazz, T instance) {
515        registerDependency(clazz, clazz.getName(), instance);
516    }
517
518    /**
519     * Registers an instance of a class to be registered as an injectable dependency.<br>
520     * The command manager will attempt to inject all fields in a command class that are annotated with
521     * {@link co.aikar.commands.annotation.Dependency} with the provided instance.
522     *
523     * @param clazz    the class the injector should look for when injecting
524     * @param key      the key which needs to be present if that
525     * @param instance the instance of the class that should be injected
526     * @throws IllegalStateException when there is already an instance for the provided class registered
527     */
528    public <T> void registerDependency(Class<? extends T> clazz, String key, T instance) {
529        if (dependencies.containsKey(clazz, key)) {
530            throw new IllegalStateException("There is already an instance of " + clazz.getName() + " with the key " + key + " registered!");
531        }
532
533        dependencies.put(clazz, key, instance);
534    }
535    
536    /**
537     * Unregisters an instance of the class, it will no longer be able to be injected
538     * 
539     * @param clazz the class the injector should look for to remove
540     * @throws IllegalStateException If the dependency was not found.
541     */
542    public <T> void unregisterDependency(Class<? extends T> clazz) {
543        unregisterDependency(clazz, clazz.getName());
544    }
545
546    /**
547     * Unregisters an instance of the class, it will no longer be able to be injected
548     * 
549     * @param clazz the class the injector should look for to remove
550     * @param key   the key which needs to be present if that
551     * @throws IllegalStateException If the dependency was not found.
552     */
553    public <T> void unregisterDependency(Class<? extends T> clazz, String key) {
554        if (!dependencies.containsKey(clazz, key)) {
555            throw new IllegalStateException("Unable to unregister a dependency of " + clazz.getName() + " with the key " + key + " because it wasn't registered");
556        }
557
558        dependencies.remove(clazz, key);
559    }
560
561    /**
562     * Attempts to inject instances of classes registered with {@link CommandManager#registerDependency(Class, Object)}
563     * into all fields of the class and its superclasses that are marked with {@link Dependency}.
564     *
565     * @param baseCommand the instance which fields should be filled
566     */
567    void injectDependencies(BaseCommand baseCommand) {
568        Class clazz = baseCommand.getClass();
569        do {
570            for (Field field : clazz.getDeclaredFields()) {
571                if (annotations.hasAnnotation(field, Dependency.class)) {
572                    String dependency = annotations.getAnnotationValue(field, Dependency.class);
573                    String key = (key = dependency).isEmpty() ? field.getType().getName() : key;
574                    Object object = dependencies.row(field.getType()).get(key);
575                    if (object == null) {
576                        throw new UnresolvedDependencyException("Could not find a registered instance of " +
577                                field.getType().getName() + " with key " + key + " for field " + field.getName() +
578                                " in class " + baseCommand.getClass().getName());
579                    }
580
581                    try {
582                        boolean accessible = field.isAccessible();
583                        if (!accessible) {
584                            field.setAccessible(true);
585                        }
586                        field.set(baseCommand, object);
587                        field.setAccessible(accessible);
588                    } catch (IllegalAccessException e) {
589                        e.printStackTrace(); //TODO should we print our own exception here to make a more descriptive error?
590                    }
591                }
592            }
593            clazz = clazz.getSuperclass();
594        } while (!clazz.equals(BaseCommand.class));
595    }
596
597    /**
598     * @deprecated Use this with caution! If you enable and use Unstable API's, your next compile using ACF
599     * may require you to update your implementation to those unstable API's
600     */
601    @Deprecated
602    public void enableUnstableAPI(String api) {
603        unstableAPIs.add(api);
604    }
605
606    void verifyUnstableAPI(String api) {
607        if (!unstableAPIs.contains(api)) {
608            throw new IllegalStateException("Using an unstable API that has not been enabled ( " + api + "). See https://acfunstable.emc.gs");
609        }
610    }
611
612    boolean hasUnstableAPI(String api) {
613        return unstableAPIs.contains(api);
614    }
615
616    Annotations getAnnotations() {
617        return annotations;
618    }
619
620    public String getCommandPrefix(CommandIssuer issuer) {
621        return "";
622    }
623}