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)); 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 */ 247 public abstract void registerCommand(BaseCommand command); 248 249 public abstract boolean hasRegisteredCommands(); 250 251 public abstract boolean isCommandIssuer(Class<?> type); 252 253 // TODO: Change this to IT if we make a breaking change 254 public abstract I getCommandIssuer(Object issuer); 255 256 public abstract RootCommand createRootCommand(String cmd); 257 258 /** 259 * Returns a Locales Manager to add and modify language tables for your commands. 260 * 261 * @return 262 */ 263 public abstract Locales getLocales(); 264 265 public boolean usingPerIssuerLocale() { 266 return usePerIssuerLocale; 267 } 268 269 public boolean usePerIssuerLocale(boolean setting) { 270 boolean old = usePerIssuerLocale; 271 usePerIssuerLocale = setting; 272 return old; 273 } 274 275 public ConditionContext createConditionContext(CommandIssuer issuer, String config) { 276 //noinspection unchecked 277 return new ConditionContext(issuer, config); 278 } 279 280 public abstract CommandExecutionContext createCommandContext(RegisteredCommand command, CommandParameter parameter, CommandIssuer sender, List<String> args, int i, Map<String, Object> passedArgs); 281 282 public abstract CommandCompletionContext createCompletionContext(RegisteredCommand command, CommandIssuer sender, String input, String config, String[] args); 283 284 public abstract void log(final LogLevel level, final String message, final Throwable throwable); 285 286 public void log(final LogLevel level, final String message) { 287 log(level, message, null); 288 } 289 290 /** 291 * Lets you add custom string replacements that can be applied to annotation values, 292 * to reduce duplication/repetition of common values such as permission nodes and command prefixes. 293 * <p> 294 * Any replacement registered starts with a % 295 * <p> 296 * So for ex @CommandPermission("%staff") 297 * 298 * @return Replacements Manager 299 */ 300 public CommandReplacements getCommandReplacements() { 301 return replacements; 302 } 303 304 public boolean hasPermission(CommandIssuer issuer, Set<String> permissions) { 305 for (String permission : permissions) { 306 if (!hasPermission(issuer, permission)) { 307 return false; 308 } 309 } 310 return true; 311 } 312 313 public boolean hasPermission(CommandIssuer issuer, String permission) { 314 if (permission == null || permission.isEmpty()) { 315 return true; 316 } 317 for (String perm : ACFPatterns.COMMA.split(permission)) { 318 if (!perm.isEmpty() && !issuer.hasPermission(perm)) { 319 return false; 320 } 321 } 322 return true; 323 } 324 325 public synchronized RootCommand getRootCommand(@NotNull String cmd) { 326 return rootCommands.get(ACFPatterns.SPACE.split(cmd.toLowerCase(Locale.ENGLISH), 2)[0]); 327 } 328 329 public synchronized RootCommand obtainRootCommand(@NotNull String cmd) { 330 return rootCommands.computeIfAbsent(ACFPatterns.SPACE.split(cmd.toLowerCase(Locale.ENGLISH), 2)[0], this::createRootCommand); 331 } 332 333 public abstract Collection<RootCommand> getRegisteredRootCommands(); 334 335 public RegisteredCommand createRegisteredCommand(BaseCommand command, String cmdName, Method method, String prefSubCommand) { 336 return new RegisteredCommand(command, cmdName, method, prefSubCommand); 337 } 338 339 /** 340 * Sets the default {@link ExceptionHandler} that is called when an exception occurs while executing a command, if the command doesn't have it's own exception handler registered. 341 * 342 * @param exceptionHandler the handler that should handle uncaught exceptions. May not be null if logExceptions is false 343 */ 344 public void setDefaultExceptionHandler(ExceptionHandler exceptionHandler) { 345 if (exceptionHandler == null && !this.logUnhandledExceptions) { 346 throw new IllegalArgumentException("You may not disable the default exception handler and have logging of unhandled exceptions disabled"); 347 } 348 defaultExceptionHandler = exceptionHandler; 349 } 350 351 /** 352 * Sets the default {@link ExceptionHandler} that is called when an exception occurs while executing a command, if the command doesn't have it's own exception handler registered, and lets you control if ACF should also log the exception still. 353 * <p> 354 * If you disable logging, you need to log it yourself in your handler. 355 * 356 * @param exceptionHandler the handler that should handle uncaught exceptions. May not be null if logExceptions is false 357 * @param logExceptions Whether or not to log exceptions. 358 */ 359 public void setDefaultExceptionHandler(ExceptionHandler exceptionHandler, boolean logExceptions) { 360 if (exceptionHandler == null && !logExceptions) { 361 throw new IllegalArgumentException("You may not disable the default exception handler and have logging of unhandled exceptions disabled"); 362 } 363 this.logUnhandledExceptions = logExceptions; 364 this.defaultExceptionHandler = exceptionHandler; 365 } 366 367 public boolean isLoggingUnhandledExceptions() { 368 return this.logUnhandledExceptions; 369 } 370 371 /** 372 * Gets the current default exception handler, might be null. 373 * 374 * @return the default exception handler 375 */ 376 public ExceptionHandler getDefaultExceptionHandler() { 377 return defaultExceptionHandler; 378 } 379 380 protected boolean handleUncaughtException(BaseCommand scope, RegisteredCommand registeredCommand, CommandIssuer sender, List<String> args, Throwable t) { 381 if (t instanceof InvocationTargetException && t.getCause() != null) { 382 t = t.getCause(); 383 } 384 boolean result = false; 385 if (scope.getExceptionHandler() != null) { 386 result = scope.getExceptionHandler().execute(scope, registeredCommand, sender, args, t); 387 } else if (defaultExceptionHandler != null) { 388 result = defaultExceptionHandler.execute(scope, registeredCommand, sender, args, t); 389 } 390 return result; 391 } 392 393 public void sendMessage(IT issuerArg, MessageType type, MessageKeyProvider key, String... replacements) { 394 sendMessage(getCommandIssuer(issuerArg), type, key, replacements); 395 } 396 397 public void sendMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) { 398 String message = formatMessage(issuer, type, key, replacements); 399 400 for (String msg : ACFPatterns.NEWLINE.split(message)) { 401 issuer.sendMessageInternal(ACFUtil.rtrim(msg)); 402 } 403 } 404 405 public String formatMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) { 406 String message = getLocales().getMessage(issuer, key.getMessageKey()); 407 if (replacements.length > 0) { 408 message = ACFUtil.replaceStrings(message, replacements); 409 } 410 411 message = getCommandReplacements().replace(message); 412 message = getLocales().replaceI18NStrings(message); 413 414 MessageFormatter formatter = formatters.getOrDefault(type, defaultFormatter); 415 if (formatter != null) { 416 message = formatter.format(message); 417 } 418 return message; 419 } 420 421 public void onLocaleChange(IssuerLocaleChangedCallback<I> onChange) { 422 localeChangedCallbacks.add(onChange); 423 } 424 425 public void notifyLocaleChange(I issuer, Locale oldLocale, Locale newLocale) { 426 localeChangedCallbacks.forEach(cb -> { 427 try { 428 cb.onIssuerLocaleChange(issuer, oldLocale, newLocale); 429 } catch (Exception e) { 430 this.log(LogLevel.ERROR, "Error in notifyLocaleChange", e); 431 } 432 }); 433 } 434 435 public Locale setIssuerLocale(IT issuer, Locale locale) { 436 I commandIssuer = getCommandIssuer(issuer); 437 438 Locale old = issuersLocale.put(commandIssuer.getUniqueId(), locale); 439 if (!Objects.equals(old, locale)) { 440 this.notifyLocaleChange(commandIssuer, old, locale); 441 } 442 443 return old; 444 } 445 446 public Locale getIssuerLocale(CommandIssuer issuer) { 447 if (usingPerIssuerLocale() && issuer != null) { 448 Locale locale = issuersLocale.get(issuer.getUniqueId()); 449 if (locale != null) { 450 return locale; 451 } 452 } 453 454 return getLocales().getDefaultLocale(); 455 } 456 457 CommandOperationContext<I> createCommandOperationContext(BaseCommand command, CommandIssuer issuer, String commandLabel, String[] args, boolean isAsync) { 458 //noinspection unchecked 459 return new CommandOperationContext<>( 460 this, 461 (I) issuer, 462 command, 463 commandLabel, 464 args, 465 isAsync 466 ); 467 } 468 469 /** 470 * Gets a list of all currently supported languages for this manager. 471 * These locales will be automatically loaded from 472 * 473 * @return 474 */ 475 public Set<Locale> getSupportedLanguages() { 476 return supportedLanguages; 477 } 478 479 /** 480 * Adds a new locale to the list of automatic Locales to load Message Bundles for. 481 * All bundles loaded under the previous supported languages will now automatically load for this new locale too. 482 * 483 * @param locale 484 */ 485 public void addSupportedLanguage(Locale locale) { 486 supportedLanguages.add(locale); 487 getLocales().loadMissingBundles(); 488 } 489 490 /** 491 * Registers an instance of a class to be registered as an injectable dependency.<br> 492 * The command manager will attempt to inject all fields in a command class that are annotated with 493 * {@link co.aikar.commands.annotation.Dependency} with the provided instance. 494 * 495 * @param clazz the class the injector should look for when injecting 496 * @param instance the instance of the class that should be injected 497 * @throws IllegalStateException when there is already an instance for the provided class registered 498 */ 499 public <T> void registerDependency(Class<? extends T> clazz, T instance) { 500 registerDependency(clazz, clazz.getName(), instance); 501 } 502 503 /** 504 * Registers an instance of a class to be registered as an injectable dependency.<br> 505 * The command manager will attempt to inject all fields in a command class that are annotated with 506 * {@link co.aikar.commands.annotation.Dependency} with the provided instance. 507 * 508 * @param clazz the class the injector should look for when injecting 509 * @param key the key which needs to be present if that 510 * @param instance the instance of the class that should be injected 511 * @throws IllegalStateException when there is already an instance for the provided class registered 512 */ 513 public <T> void registerDependency(Class<? extends T> clazz, String key, T instance) { 514 if (dependencies.containsKey(clazz, key)) { 515 throw new IllegalStateException("There is already an instance of " + clazz.getName() + " with the key " + key + " registered!"); 516 } 517 518 dependencies.put(clazz, key, instance); 519 } 520 521 /** 522 * Attempts to inject instances of classes registered with {@link CommandManager#registerDependency(Class, Object)} 523 * into all fields of the class and its superclasses that are marked with {@link Dependency}. 524 * 525 * @param baseCommand the instance which fields should be filled 526 */ 527 void injectDependencies(BaseCommand baseCommand) { 528 Class clazz = baseCommand.getClass(); 529 do { 530 for (Field field : clazz.getDeclaredFields()) { 531 if (annotations.hasAnnotation(field, Dependency.class)) { 532 String dependency = annotations.getAnnotationValue(field, Dependency.class); 533 String key = (key = dependency).isEmpty() ? field.getType().getName() : key; 534 Object object = dependencies.row(field.getType()).get(key); 535 if (object == null) { 536 throw new UnresolvedDependencyException("Could not find a registered instance of " + 537 field.getType().getName() + " with key " + key + " for field " + field.getName() + 538 " in class " + baseCommand.getClass().getName()); 539 } 540 541 try { 542 boolean accessible = field.isAccessible(); 543 if (!accessible) { 544 field.setAccessible(true); 545 } 546 field.set(baseCommand, object); 547 field.setAccessible(accessible); 548 } catch (IllegalAccessException e) { 549 e.printStackTrace(); //TODO should we print our own exception here to make a more descriptive error? 550 } 551 } 552 } 553 clazz = clazz.getSuperclass(); 554 } while (!clazz.equals(BaseCommand.class)); 555 } 556 557 /** 558 * @deprecated Use this with caution! If you enable and use Unstable API's, your next compile using ACF 559 * may require you to update your implementation to those unstable API's 560 */ 561 @Deprecated 562 public void enableUnstableAPI(String api) { 563 unstableAPIs.add(api); 564 } 565 566 void verifyUnstableAPI(String api) { 567 if (!unstableAPIs.contains(api)) { 568 throw new IllegalStateException("Using an unstable API that has not been enabled ( " + api + "). See https://acfunstable.emc.gs"); 569 } 570 } 571 572 boolean hasUnstableAPI(String api) { 573 return unstableAPIs.contains(api); 574 } 575 576 Annotations getAnnotations() { 577 return annotations; 578 } 579 580 public String getCommandPrefix(CommandIssuer issuer) { 581 return ""; 582 } 583}