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