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