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.locales.MessageKeyProvider; 027import com.google.common.collect.Sets; 028import org.jetbrains.annotations.NotNull; 029 030import java.lang.reflect.Method; 031import java.lang.reflect.Parameter; 032import java.util.*; 033 034@SuppressWarnings("WeakerAccess") 035public abstract class CommandManager <I, FT, F extends MessageFormatter<FT>> { 036 037 /** 038 * This is a stack incase a command calls a command 039 */ 040 static ThreadLocal<Stack<CommandOperationContext>> commandOperationContext = ThreadLocal.withInitial(() -> { 041 return new Stack<CommandOperationContext>() { 042 @Override 043 public synchronized CommandOperationContext peek() { 044 return super.size() == 0 ? null : super.peek(); 045 } 046 }; 047 }); 048 protected Map<String, RootCommand> rootCommands = new HashMap<>(); 049 protected CommandReplacements replacements = new CommandReplacements(this); 050 protected ExceptionHandler defaultExceptionHandler = null; 051 052 protected Set<Locale> supportedLanguages = Sets.newHashSet(Locales.ENGLISH, Locales.GERMAN, Locales.SPANISH, Locales.CZECH); 053 protected Map<MessageType, F> formatters = new IdentityHashMap<>(); 054 protected F defaultFormatter; 055 private Set<String> unstableAPIs = Sets.newHashSet(); 056 057 public static CommandOperationContext getCurrentCommandOperationContext() { 058 return commandOperationContext.get().peek(); 059 } 060 061 public static CommandIssuer getCurrentCommandIssuer() { 062 CommandOperationContext context = commandOperationContext.get().peek(); 063 return context != null ? context.getCommandIssuer() : null; 064 } 065 066 public static CommandManager getCurrentCommandManager() { 067 CommandOperationContext context = commandOperationContext.get().peek(); 068 return context != null ? context.getCommandManager() : null; 069 } 070 071 public F setFormat(MessageType type, F formatter) { 072 return formatters.put(type, formatter); 073 } 074 075 public F getFormat(MessageType type) { 076 return formatters.getOrDefault(type, defaultFormatter); 077 } 078 079 public void setFormat(MessageType type, FT... colors) { 080 F format = getFormat(type); 081 for (int i = 0; i < colors.length; i++) { 082 format.setColor(i, colors[i]); 083 } 084 } 085 086 public void setFormat(MessageType type, int i, FT color) { 087 F format = getFormat(type); 088 format.setColor(i, color); 089 } 090 091 public F getDefaultFormatter() { 092 return defaultFormatter; 093 } 094 095 public void setDefaultFormatter(F defaultFormatter) { 096 this.defaultFormatter = defaultFormatter; 097 } 098 099 /** 100 * Gets the command contexts manager 101 * @return Command Contexts 102 */ 103 public abstract CommandContexts<?> getCommandContexts(); 104 105 /** 106 * Gets the command completions manager 107 * @return Command Completions 108 */ 109 public abstract CommandCompletions<?> getCommandCompletions(); 110 111 /** @deprecated Unstable API */ @Deprecated @UnstableAPI 112 public CommandHelp generateCommandHelp(@NotNull String command) { 113 verifyUnstableAPI("help"); 114 CommandOperationContext context = getCurrentCommandOperationContext(); 115 if (context == null) { 116 throw new IllegalStateException("This method can only be called as part of a command execution."); 117 } 118 return generateCommandHelp(context.getCommandIssuer(), command); 119 } 120 121 /** @deprecated Unstable API */ @Deprecated @UnstableAPI 122 public CommandHelp generateCommandHelp(CommandIssuer issuer, @NotNull String command) { 123 verifyUnstableAPI("help"); 124 return generateCommandHelp(issuer, obtainRootCommand(ACFPatterns.SPACE.split(command, 2)[0])); 125 } 126 127 /** @deprecated Unstable API */ @Deprecated @UnstableAPI 128 public CommandHelp generateCommandHelp() { 129 verifyUnstableAPI("help"); 130 CommandOperationContext context = getCurrentCommandOperationContext(); 131 if (context == null) { 132 throw new IllegalStateException("This method can only be called as part of a command execution."); 133 } 134 String commandLabel = context.getCommandLabel(); 135 return generateCommandHelp(context.getCommandIssuer(), this.obtainRootCommand(commandLabel)); 136 } 137 138 /** @deprecated Unstable API */ @Deprecated @UnstableAPI 139 public CommandHelp generateCommandHelp(CommandIssuer issuer, RootCommand rootCommand) { 140 verifyUnstableAPI("help"); 141 return new CommandHelp(this, rootCommand, issuer); 142 } 143 144 /** 145 * Registers a command with ACF 146 * 147 * @param command The command to register 148 * @return boolean 149 */ 150 public abstract void registerCommand(BaseCommand command); 151 public abstract boolean hasRegisteredCommands(); 152 public abstract boolean isCommandIssuer(Class<?> type); 153 154 // TODO: Change this to I if we make a breaking change 155 public abstract CommandIssuer getCommandIssuer(Object issuer); 156 157 public abstract RootCommand createRootCommand(String cmd); 158 159 /** 160 * Returns a Locales Manager to add and modify language tables for your commands. 161 * @return 162 */ 163 public abstract Locales getLocales(); 164 165 public abstract <R extends CommandExecutionContext> R createCommandContext(RegisteredCommand command, Parameter parameter, CommandIssuer sender, List<String> args, int i, Map<String, Object> passedArgs); 166 167 public abstract CommandCompletionContext createCompletionContext(RegisteredCommand command, CommandIssuer sender, String input, String config, String[] args); 168 169 public abstract void log(final LogLevel level, final String message, final Throwable throwable); 170 171 public void log(final LogLevel level, final String message) { 172 log(level, message, null); 173 } 174 175 /** 176 * Lets you add custom string replacements that can be applied to annotation values, 177 * to reduce duplication/repetition of common values such as permission nodes and command prefixes. 178 * 179 * Any replacement registered starts with a % 180 * 181 * So for ex @CommandPermission("%staff") 182 * @return Replacements Manager 183 */ 184 public CommandReplacements getCommandReplacements() { 185 return replacements; 186 } 187 188 public boolean hasPermission(CommandIssuer issuer, String permission) { 189 return permission == null || permission.isEmpty() || issuer.hasPermission(permission); 190 } 191 192 public synchronized RootCommand obtainRootCommand(String cmd) { 193 return rootCommands.computeIfAbsent(cmd.toLowerCase(), this::createRootCommand); 194 } 195 196 public RegisteredCommand createRegisteredCommand(BaseCommand command, String cmdName, Method method, String prefSubCommand) { 197 return new RegisteredCommand(command, cmdName, method, prefSubCommand); 198 } 199 200 /** 201 * 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. 202 * 203 * @param exceptionHandler the handler that should handle uncaught exceptions 204 */ 205 public void setDefaultExceptionHandler(ExceptionHandler exceptionHandler) { 206 defaultExceptionHandler = exceptionHandler; 207 } 208 209 /** 210 * Gets the current default exception handler, might be null. 211 * 212 * @return the default exception handler 213 */ 214 public ExceptionHandler getDefaultExceptionHandler() { 215 return defaultExceptionHandler; 216 } 217 218 protected boolean handleUncaughtException(BaseCommand scope, RegisteredCommand registeredCommand, CommandIssuer sender, List<String> args, Throwable t) { 219 boolean result = false; 220 if (scope.getExceptionHandler() != null) { 221 result = scope.getExceptionHandler().execute(scope, registeredCommand, sender, args, t); 222 } else if (defaultExceptionHandler != null) { 223 result = defaultExceptionHandler.execute(scope, registeredCommand, sender, args, t); 224 } 225 return result; 226 } 227 228 public void sendMessage(I issuerArg, MessageType type, MessageKeyProvider key, String... replacements) { 229 sendMessage(getCommandIssuer(issuerArg), type, key, replacements); 230 } 231 232 public void sendMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) { 233 String message = formatMessage(issuer, type, key, replacements); 234 235 for (String msg : ACFPatterns.NEWLINE.split(message)) { 236 issuer.sendMessageInternal(ACFUtil.rtrim(msg)); 237 } 238 } 239 240 public String formatMessage(CommandIssuer issuer, MessageType type, MessageKeyProvider key, String... replacements) { 241 String message = getLocales().getMessage(issuer, key.getMessageKey()); 242 if (replacements.length > 0) { 243 message = ACFUtil.replaceStrings(message, replacements); 244 } 245 246 message = getCommandReplacements().replace(message); 247 248 MessageFormatter formatter = formatters.getOrDefault(type, defaultFormatter); 249 if (formatter != null) { 250 message = formatter.format(message); 251 } 252 return message; 253 } 254 255 public Locale getIssuerLocale(CommandIssuer issuer) { 256 return getLocales().getDefaultLocale(); 257 } 258 259 260 public CommandOperationContext createCommandOperationContext(BaseCommand command, CommandIssuer issuer, String commandLabel, String[] args) { 261 return new CommandOperationContext( 262 this, 263 issuer, 264 command, 265 commandLabel, 266 args 267 ); 268 } 269 270 /** 271 * Gets a list of all currently supported languages for this manager. 272 * These locales will be automatically loaded from 273 * @return 274 */ 275 public Set<Locale> getSupportedLanguages() { 276 return supportedLanguages; 277 } 278 279 /** 280 * Adds a new locale to the list of automatic Locales to load Message Bundles for. 281 * All bundles loaded under the previous supported languages will now automatically load for this new locale too. 282 * 283 * @param locale 284 */ 285 public void addSupportedLanguage(Locale locale) { 286 supportedLanguages.add(locale); 287 getLocales().loadMissingBundles(); 288 } 289 290 /** 291 * @deprecated Use this with caution! If you enable and use Unstable API's, your next compile using ACF 292 * may require you to update your implementation to those unstable API's 293 */ 294 @Deprecated 295 public void enableUnstableAPI(String api) { 296 unstableAPIs.add(api); 297 } 298 void verifyUnstableAPI(String api) { 299 if (!unstableAPIs.contains(api)) { 300 throw new IllegalStateException("Using an unstable API that has not been enabled ( " + api + "). See https://acfunstable.emc.gs"); 301 } 302 } 303}