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.commands.annotation.HelpCommand; 028import co.aikar.locales.MessageKeyProvider; 029import com.google.common.collect.HashBasedTable; 030import com.google.common.collect.Lists; 031import com.google.common.collect.Maps; 032import com.google.common.collect.Sets; 033import com.google.common.collect.Table; 034import org.jetbrains.annotations.NotNull; 035 036import java.lang.annotation.Annotation; 037import java.lang.reflect.Field; 038import java.lang.reflect.InvocationTargetException; 039import java.lang.reflect.Method; 040import java.util.HashMap; 041import java.util.IdentityHashMap; 042import java.util.List; 043import java.util.Locale; 044import java.util.Map; 045import java.util.Objects; 046import java.util.Set; 047import java.util.Stack; 048import java.util.UUID; 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 protected Table<Class<?>, String, Object> dependencies = HashBasedTable.create(); 075 protected CommandHelpFormatter helpFormatter = new CommandHelpFormatter(this); 076 077 protected boolean usePerIssuerLocale = false; 078 protected List<IssuerLocaleChangedCallback<I>> localeChangedCallbacks = Lists.newArrayList(); 079 protected Set<Locale> supportedLanguages = Sets.newHashSet(Locales.ENGLISH, Locales.GERMAN, Locales.SPANISH, Locales.CZECH, Locales.PORTUGUESE, Locales.SWEDISH, Locales.NORWEGIAN_BOKMAAL, Locales.NORWEGIAN_NYNORSK); 080 protected Map<MessageType, MF> formatters = new IdentityHashMap<>(); 081 protected MF defaultFormatter; 082 protected int defaultHelpPerPage = 10; 083 084 protected Map<UUID, Locale> issuersLocale = Maps.newConcurrentMap(); 085 086 private Set<String> unstableAPIs = Sets.newHashSet(); 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 RegisteredCommand createRegisteredCommand(BaseCommand command, String cmdName, Method method, String prefSubCommand) { 290 return new RegisteredCommand(command, cmdName, method, prefSubCommand); 291 } 292 293 /** 294 * 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. 295 * 296 * @param exceptionHandler the handler that should handle uncaught exceptions 297 */ 298 public void setDefaultExceptionHandler(ExceptionHandler exceptionHandler) { 299 defaultExceptionHandler = exceptionHandler; 300 } 301 302 /** 303 * Gets the current default exception handler, might be null. 304 * 305 * @return the default exception handler 306 */ 307 public ExceptionHandler getDefaultExceptionHandler() { 308 return defaultExceptionHandler; 309 } 310 311 protected boolean handleUncaughtException(BaseCommand scope, RegisteredCommand registeredCommand, CommandIssuer sender, List<String> args, Throwable t) { 312 if (t instanceof InvocationTargetException && t.getCause() != null) { 313 t = t.getCause(); 314 } 315 boolean result = false; 316 if (scope.getExceptionHandler() != null) { 317 result = scope.getExceptionHandler().execute(scope, registeredCommand, sender, args, t); 318 } else if (defaultExceptionHandler != null) { 319 result = defaultExceptionHandler.execute(scope, registeredCommand, sender, args, t); 320 } 321 return result; 322 } 323 324 public void sendMessage(IT issuerArg, MessageType type, MessageKeyProvider key, String... replacements) { 325 sendMessage(getCommandIssuer(issuerArg), type, key, replacements); 326 } 327 328 public void sendMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) { 329 String message = formatMessage(issuer, type, key, replacements); 330 331 for (String msg : ACFPatterns.NEWLINE.split(message)) { 332 issuer.sendMessageInternal(ACFUtil.rtrim(msg)); 333 } 334 } 335 336 public String formatMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) { 337 String message = getLocales().getMessage(issuer, key.getMessageKey()); 338 if (replacements.length > 0) { 339 message = ACFUtil.replaceStrings(message, replacements); 340 } 341 342 message = getCommandReplacements().replace(message); 343 344 MessageFormatter formatter = formatters.getOrDefault(type, defaultFormatter); 345 if (formatter != null) { 346 message = formatter.format(message); 347 } 348 return message; 349 } 350 351 public void onLocaleChange(IssuerLocaleChangedCallback<I> onChange) { 352 localeChangedCallbacks.add(onChange); 353 } 354 355 public void notifyLocaleChange(I issuer, Locale oldLocale, Locale newLocale) { 356 localeChangedCallbacks.forEach(cb -> { 357 try { 358 cb.onIssuerLocaleChange(issuer, oldLocale, newLocale); 359 } catch (Exception e) { 360 this.log(LogLevel.ERROR, "Error in notifyLocaleChange", e); 361 } 362 }); 363 } 364 365 public Locale setIssuerLocale(IT issuer, Locale locale) { 366 I commandIssuer = getCommandIssuer(issuer); 367 368 Locale old = issuersLocale.put(commandIssuer.getUniqueId(), locale); 369 if (!Objects.equals(old, locale)) { 370 this.notifyLocaleChange(commandIssuer, old, locale); 371 } 372 373 return old; 374 } 375 376 public Locale getIssuerLocale(CommandIssuer issuer) { 377 if (usingPerIssuerLocale()) { 378 Locale locale = issuersLocale.get(issuer.getUniqueId()); 379 if (locale != null) { 380 return locale; 381 } 382 } 383 384 return getLocales().getDefaultLocale(); 385 } 386 387 CommandOperationContext<I> createCommandOperationContext(BaseCommand command, CommandIssuer issuer, String commandLabel, String[] args, boolean isAsync) { 388 //noinspection unchecked 389 return new CommandOperationContext<>( 390 this, 391 (I) issuer, 392 command, 393 commandLabel, 394 args, 395 isAsync 396 ); 397 } 398 399 /** 400 * Gets a list of all currently supported languages for this manager. 401 * These locales will be automatically loaded from 402 * @return 403 */ 404 public Set<Locale> getSupportedLanguages() { 405 return supportedLanguages; 406 } 407 408 /** 409 * Adds a new locale to the list of automatic Locales to load Message Bundles for. 410 * All bundles loaded under the previous supported languages will now automatically load for this new locale too. 411 * 412 * @param locale 413 */ 414 public void addSupportedLanguage(Locale locale) { 415 supportedLanguages.add(locale); 416 getLocales().loadMissingBundles(); 417 } 418 419 /** 420 * Registers an instance of a class to be registered as an injectable dependency.<br> 421 * The command manager will attempt to inject all fields in a command class that are annotated with 422 * {@link co.aikar.commands.annotation.Dependency} with the provided instance. 423 * 424 * @param clazz the class the injector should look for when injecting 425 * @param instance the instance of the class that should be injected 426 * @throws IllegalStateException when there is already an instance for the provided class registered 427 */ 428 public <T> void registerDependency(Class<? extends T> clazz, T instance){ 429 registerDependency(clazz, clazz.getName(), instance); 430 } 431 432 /** 433 * Registers an instance of a class to be registered as an injectable dependency.<br> 434 * The command manager will attempt to inject all fields in a command class that are annotated with 435 * {@link co.aikar.commands.annotation.Dependency} with the provided instance. 436 * 437 * @param clazz the class the injector should look for when injecting 438 * @param key the key which needs to be present if that 439 * @param instance the instance of the class that should be injected 440 * @throws IllegalStateException when there is already an instance for the provided class registered 441 */ 442 public <T> void registerDependency(Class<? extends T> clazz, String key, T instance){ 443 if(dependencies.containsRow(clazz) && dependencies.containsColumn(key)){ 444 throw new IllegalStateException("There is already an instance of " + clazz.getName() + " with the key " + key + " registered!"); 445 } 446 447 dependencies.put(clazz, key, instance); 448 } 449 450 /** 451 * Attempts to inject instances of classes registered with {@link CommandManager#registerDependency(Class, Object)} 452 * into all fields of the class and its superclasses that are marked with {@link Dependency}. 453 * 454 * @param baseCommand the instance which fields should be filled 455 */ 456 void injectDependencies(BaseCommand baseCommand) { 457 Class clazz = baseCommand.getClass(); 458 do { 459 for (Field field : clazz.getDeclaredFields()) { 460 if (annotations.hasAnnotation(field, Dependency.class)) { 461 String dependency = annotations.getAnnotationValue(field, Dependency.class); 462 String key = (key = dependency).isEmpty() ? field.getType().getName() : key; 463 Object object = dependencies.row(field.getType()).get(key); 464 if (object == null) { 465 throw new UnresolvedDependencyException("Could not find a registered instance of " + 466 field.getType().getName() + " with key " + key + " for field " + field.getName() + 467 " in class " + baseCommand.getClass().getName()); 468 } 469 470 try { 471 boolean accessible = field.isAccessible(); 472 if (!accessible) { 473 field.setAccessible(true); 474 } 475 field.set(baseCommand, object); 476 field.setAccessible(accessible); 477 } catch (IllegalAccessException e) { 478 e.printStackTrace(); //TODO should we print our own exception here to make a more descriptive error? 479 } 480 } 481 } 482 clazz = clazz.getSuperclass(); 483 } while (!clazz.equals(BaseCommand.class)); 484 } 485 486 /** 487 * @deprecated Use this with caution! If you enable and use Unstable API's, your next compile using ACF 488 * may require you to update your implementation to those unstable API's 489 */ 490 @Deprecated 491 public void enableUnstableAPI(String api) { 492 unstableAPIs.add(api); 493 } 494 void verifyUnstableAPI(String api) { 495 if (!unstableAPIs.contains(api)) { 496 throw new IllegalStateException("Using an unstable API that has not been enabled ( " + api + "). See https://acfunstable.emc.gs"); 497 } 498 } 499 500 boolean hasUnstableAPI(String api) { 501 return unstableAPIs.contains(api); 502 } 503 504 Annotations getAnnotations() { 505 return annotations; 506 } 507 508 public String getCommandPrefix(CommandIssuer issuer) { 509 return ""; 510 } 511}