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}