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.*; 027import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil; 028import com.google.common.collect.HashMultimap; 029import com.google.common.collect.ImmutableList; 030import com.google.common.collect.Iterables; 031import com.google.common.collect.Lists; 032import com.google.common.collect.SetMultimap; 033 034import java.lang.reflect.Constructor; 035import java.lang.reflect.InvocationTargetException; 036import java.lang.reflect.Method; 037import java.lang.reflect.Parameter; 038import java.util.*; 039import java.util.Optional; 040import java.util.stream.Collectors; 041import java.util.stream.Stream; 042 043@SuppressWarnings("unused") 044public abstract class BaseCommand { 045 046 public static final String UNKNOWN = "__unknown"; 047 public static final String DEFAULT = "__default"; 048 final SetMultimap<String, RegisteredCommand> subCommands = HashMultimap.create(); 049 private Method preCommandHandler; 050 051 @SuppressWarnings("WeakerAccess") 052 private String execLabel; 053 @SuppressWarnings("WeakerAccess") 054 private String execSubcommand; 055 @SuppressWarnings("WeakerAccess") 056 private String[] origArgs; 057 CommandManager<?, ?, ?> manager = null; 058 BaseCommand parentCommand; 059 Map<String, RootCommand> registeredCommands = new HashMap<>(); 060 String description; 061 String commandName; 062 String usageMessage; 063 String permission; 064 065 private ExceptionHandler exceptionHandler = null; 066 CommandOperationContext lastCommandOperationContext; 067 068 public BaseCommand() {} 069 public BaseCommand(String cmd) { 070 this.commandName = cmd; 071 } 072 073 /** 074 * Gets the root command name that the user actually typed 075 * @return Name 076 */ 077 public String getExecCommandLabel() { 078 return execLabel; 079 } 080 081 /** 082 * Gets the actual sub command name the user typed 083 * @return Name 084 */ 085 public String getExecSubcommand() { 086 return execSubcommand; 087 } 088 089 /** 090 * Gets the actual args in string form the user typed 091 * @return Args 092 */ 093 public String[] getOrigArgs() { 094 return origArgs; 095 } 096 097 void setParentCommand(BaseCommand command) { 098 this.parentCommand = command; 099 } 100 void onRegister(CommandManager manager) { 101 onRegister(manager, this.commandName); 102 } 103 void onRegister(CommandManager manager, String cmd) { 104 this.manager = manager; 105 final Class<? extends BaseCommand> self = this.getClass(); 106 CommandAlias rootCmdAliasAnno = self.getAnnotation(CommandAlias.class); 107 String rootCmdAlias = rootCmdAliasAnno != null ? manager.getCommandReplacements().replace(rootCmdAliasAnno.value()).toLowerCase() : null; 108 if (cmd == null && rootCmdAlias != null) { 109 cmd = ACFPatterns.PIPE.split(rootCmdAlias)[0]; 110 } 111 this.commandName = cmd != null ? cmd : self.getSimpleName().toLowerCase(); 112 113 this.description = this.commandName + " commands"; 114 this.usageMessage = "/" + this.commandName; 115 116 final CommandPermission perm = self.getAnnotation(CommandPermission.class); 117 if (perm != null) { 118 this.permission = manager.getCommandReplacements().replace(perm.value()); 119 } 120 121 boolean foundDefault = false; 122 boolean foundUnknown = false; 123 for (Method method : self.getDeclaredMethods()) { 124 method.setAccessible(true); 125 String sublist = null; 126 String sub = getSubcommandValue(method); 127 final Default def = method.getAnnotation(Default.class); 128 final HelpCommand helpCommand = method.getAnnotation(HelpCommand.class); 129 130 final CommandAlias commandAliases = method.getAnnotation(CommandAlias.class); 131 132 if (def != null || (!foundDefault && helpCommand != null)) { 133 if (!foundDefault) { 134 registerSubcommand(method, DEFAULT); 135 if (def != null) { 136 foundDefault = true; 137 } 138 } else { 139 ACFUtil.sneaky(new IllegalStateException("Multiple @Default/@HelpCommand commands, duplicate on " + method.getDeclaringClass().getName() + "#" + method.getName())); 140 } 141 } 142 143 if (sub != null) { 144 sublist = sub; 145 } else if (commandAliases != null) { 146 sublist = commandAliases.value(); 147 } else if (helpCommand != null) { 148 sublist = helpCommand.value(); 149 } 150 151 UnknownHandler unknown = method.getAnnotation(UnknownHandler.class); 152 PreCommand preCommand = method.getAnnotation(PreCommand.class); 153 if (unknown != null || (!foundUnknown && helpCommand != null)) { 154 if (!foundUnknown) { 155 registerSubcommand(method, UNKNOWN); 156 if (unknown != null) { 157 foundUnknown = true; 158 } 159 } else { 160 ACFUtil.sneaky(new IllegalStateException("Multiple @UnknownHandler/@HelpCommand commands, duplicate on " + method.getDeclaringClass().getName() + "#" + method.getName())); 161 } 162 } else if (preCommand != null) { 163 if (this.preCommandHandler == null) { 164 this.preCommandHandler = method; 165 } else { 166 ACFUtil.sneaky(new IllegalStateException("Multiple @PreCommand commands, duplicate on " + method.getDeclaringClass().getName() + "#" + method.getName())); 167 } 168 } 169 if (sublist != null) { 170 registerSubcommand(method, sublist); 171 } 172 } 173 174 if (rootCmdAlias != null) { 175 Set<String> cmdList = new HashSet<>(); 176 Collections.addAll(cmdList, ACFPatterns.PIPE.split(rootCmdAlias)); 177 cmdList.remove(cmd); 178 for (String cmdAlias : cmdList) { 179 register(cmdAlias, this); 180 } 181 } 182 183 if (cmd != null) { 184 register(cmd, this); 185 } 186 for (Class<?> clazz : this.getClass().getDeclaredClasses()) { 187 if (BaseCommand.class.isAssignableFrom(clazz)) { 188 try { 189 BaseCommand subCommand = null; 190 Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors(); 191 for (Constructor<?> declaredConstructor : declaredConstructors) { 192 193 declaredConstructor.setAccessible(true); 194 Parameter[] parameters = declaredConstructor.getParameters(); 195 if (parameters.length == 1) { 196 subCommand = (BaseCommand) declaredConstructor.newInstance(this); 197 } else { 198 manager.log(LogLevel.INFO, "Found unusable constructor: " + declaredConstructor.getName() + "(" + Stream.of(parameters).map(p -> p.getType().getSimpleName() + " " + p.getName()).collect(Collectors.joining("<c2>,</c2> ")) + ")"); 199 } 200 } 201 if (subCommand != null) { 202 subCommand.setParentCommand(this); 203 subCommand.onRegister(manager, cmd); 204 this.subCommands.putAll(subCommand.subCommands); 205 this.registeredCommands.putAll(subCommand.registeredCommands); 206 } else { 207 this.manager.log(LogLevel.ERROR, "Could not find a subcommand ctor for " + clazz.getName()); 208 } 209 } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 210 e.printStackTrace(); 211 } 212 } 213 } 214 215 } 216 217 private String getSubcommandValue(Method method) { 218 final Subcommand sub = method.getAnnotation(Subcommand.class); 219 if (sub == null) { 220 return null; 221 } 222 List<String> subList = new ArrayList<>(); 223 subList.add(sub.value()); 224 Class<?> clazz = method.getDeclaringClass(); 225 while (clazz != null) { 226 Subcommand classSub = clazz.getAnnotation(Subcommand.class); 227 if (classSub != null) { 228 subList.add(classSub.value()); 229 } 230 clazz = clazz.getEnclosingClass(); 231 } 232 Collections.reverse(subList); 233 return ACFUtil.join(subList, " "); 234 } 235 236 private void register(String name, BaseCommand cmd) { 237 String nameLower = name.toLowerCase(); 238 RootCommand rootCommand = manager.obtainRootCommand(nameLower); 239 rootCommand.addChild(cmd); 240 241 this.registeredCommands.put(nameLower, rootCommand); 242 } 243 244 private void registerSubcommand(Method method, String subCommand) { 245 subCommand = manager.getCommandReplacements().replace(subCommand.toLowerCase()); 246 final String[] subCommandParts = ACFPatterns.SPACE.split(subCommand); 247 // Must run getSubcommandPossibility BEFORE we rewrite it just after this. 248 Set<String> cmdList = getSubCommandPossibilityList(subCommandParts); 249 250 // Strip pipes off for auto complete addition 251 for (int i = 0; i < subCommandParts.length; i++) { 252 subCommandParts[i] = ACFPatterns.PIPE.split(subCommandParts[i])[0]; 253 } 254 String prefSubCommand = ApacheCommonsLangUtil.join(subCommandParts, " "); 255 final CommandAlias cmdAlias = method.getAnnotation(CommandAlias.class); 256 257 final String[] aliasNames = cmdAlias != null ? ACFPatterns.PIPE.split(manager.getCommandReplacements().replace(cmdAlias.value().toLowerCase())) : null; 258 String cmdName = aliasNames != null ? aliasNames[0] : this.commandName + " "; 259 RegisteredCommand cmd = manager.createRegisteredCommand(this, cmdName, method, prefSubCommand); 260 261 for (String subcmd : cmdList) { 262 subCommands.put(subcmd, cmd); 263 } 264 cmd.addSubcommands(cmdList); 265 266 if (aliasNames != null) { 267 for (String name : aliasNames) { 268 register(name, new ForwardingCommand(this, subCommandParts)); 269 } 270 } 271 } 272 273 /** 274 * Takes a string like "foo|bar baz|qux" and generates a list of 275 * - foo baz 276 * - foo qux 277 * - bar baz 278 * - bar qux 279 * 280 * For every possible sub command combination 281 * 282 * @param subCommandParts 283 * @return List of all sub command possibilities 284 */ 285 private static Set<String> getSubCommandPossibilityList(String[] subCommandParts) { 286 int i = 0; 287 Set<String> current = null; 288 while (true) { 289 Set<String> newList = new HashSet<>(); 290 291 if (i < subCommandParts.length) { 292 for (String s1 : ACFPatterns.PIPE.split(subCommandParts[i])) { 293 if (current != null) { 294 newList.addAll(current.stream().map(s -> s + " " + s1).collect(Collectors.toList())); 295 } else { 296 newList.add(s1); 297 } 298 } 299 } 300 301 if (i + 1 < subCommandParts.length) { 302 current = newList; 303 i = i + 1; 304 continue; 305 } 306 307 return newList; 308 } 309 } 310 311 public void execute(CommandIssuer issuer, String commandLabel, String[] args) { 312 commandLabel = commandLabel.toLowerCase(); 313 try { 314 CommandOperationContext commandContext = preCommandOperation(issuer, commandLabel, args); 315 316 if (args.length > 0) { 317 CommandSearch cmd = findSubCommand(args); 318 if (cmd != null) { 319 execSubcommand = cmd.getCheckSub(); 320 final String[] execargs = Arrays.copyOfRange(args, cmd.argIndex, args.length); 321 executeCommand(commandContext, issuer, execargs, cmd.cmd); 322 return; 323 } 324 } 325 326 if (subCommands.get(DEFAULT) != null) { 327 executeSubcommand(commandContext, DEFAULT, issuer, args); 328 return; 329 } 330 331 if (!executeSubcommand(commandContext, UNKNOWN, issuer, args)) { 332 help(issuer, args); 333 } 334 } finally { 335 postCommandOperation(); 336 } 337 } 338 339 private void postCommandOperation() { 340 CommandManager.commandOperationContext.get().pop(); 341 execSubcommand = null; 342 execLabel = null; 343 origArgs = new String[]{}; 344 } 345 346 private CommandOperationContext preCommandOperation(CommandIssuer issuer, String commandLabel, String[] args) { 347 Stack<CommandOperationContext> contexts = CommandManager.commandOperationContext.get(); 348 CommandOperationContext context = this.manager.createCommandOperationContext(this, issuer, commandLabel, args); 349 contexts.push(context); 350 lastCommandOperationContext = context; 351 execSubcommand = null; 352 execLabel = commandLabel; 353 origArgs = args; 354 return context; 355 } 356 357 public CommandIssuer getCurrentCommandIssuer() { 358 return CommandManager.getCurrentCommandIssuer(); 359 } 360 public CommandManager getCurrentCommandManager() { 361 return CommandManager.getCurrentCommandManager(); 362 } 363 364 private CommandSearch findSubCommand(String[] args) { 365 return findSubCommand(args, false); 366 } 367 private CommandSearch findSubCommand(String[] args, boolean completion) { 368 for (int i = args.length; i >= 0; i--) { 369 String checkSub = ApacheCommonsLangUtil.join(args, " ", 0, i).toLowerCase(); 370 Set<RegisteredCommand> cmds = subCommands.get(checkSub); 371 372 final int extraArgs = args.length - i; 373 if (!cmds.isEmpty()) { 374 RegisteredCommand cmd = null; 375 if (cmds.size() == 1) { 376 cmd = Iterables.getOnlyElement(cmds); 377 } else { 378 Optional<RegisteredCommand> optCmd = cmds.stream().filter(c -> { 379 int required = c.requiredResolvers; 380 int optional = c.optionalResolvers; 381 return extraArgs <= required + optional && (completion || extraArgs >= required); 382 }).sorted((c1, c2) -> { 383 int a = c1.requiredResolvers + c1.optionalResolvers; 384 int b = c2.requiredResolvers + c2.optionalResolvers; 385 386 if (a == b) { 387 return 0; 388 } 389 return a < b ? 1 : -1; 390 }).findFirst(); 391 if (optCmd.isPresent()) { 392 cmd = optCmd.get(); 393 } 394 } 395 if (cmd != null) { 396 return new CommandSearch(cmd, i, checkSub); 397 } 398 } 399 } 400 return null; 401 } 402 403 private void executeCommand(CommandOperationContext commandOperationContext, 404 CommandIssuer issuer, String[] args, RegisteredCommand cmd) { 405 if (cmd.hasPermission(issuer)) { 406 commandOperationContext.setRegisteredCommand(cmd); 407 if (checkPrecommand(commandOperationContext, cmd, issuer, args)) { 408 return; 409 } 410 List<String> sargs = Lists.newArrayList(args); 411 cmd.invoke(issuer, sargs); 412 } else { 413 issuer.sendMessage(MessageType.ERROR, MessageKeys.PERMISSION_DENIED); 414 } 415 } 416 417 public boolean canExecute(CommandIssuer issuer, RegisteredCommand<?> cmd) { 418 return true; 419 } 420 421 public List<String> tabComplete(CommandIssuer issuer, String commandLabel, String[] args) 422 throws IllegalArgumentException { 423 424 commandLabel = commandLabel.toLowerCase(); 425 try { 426 CommandOperationContext commandOperationContext = preCommandOperation(issuer, commandLabel, args); 427 428 final CommandSearch search = findSubCommand(args, true); 429 430 String argString = ApacheCommonsLangUtil.join(args, " ").toLowerCase(); 431 432 final List<String> cmds = new ArrayList<>(); 433 434 if (search != null) { 435 cmds.addAll(completeCommand(commandOperationContext, issuer, search.cmd, Arrays.copyOfRange(args, search.argIndex, args.length), commandLabel)); 436 } else if (subCommands.get(UNKNOWN).size() == 1) { 437 cmds.addAll(completeCommand(commandOperationContext, issuer, Iterables.getOnlyElement(subCommands.get(UNKNOWN)), args, commandLabel)); 438 } 439 440 for (Map.Entry<String, RegisteredCommand> entry : subCommands.entries()) { 441 final String key = entry.getKey(); 442 if (key.startsWith(argString) && !UNKNOWN.equals(key) && !DEFAULT.equals(key)) { 443 final RegisteredCommand value = entry.getValue(); 444 if (!value.hasPermission(issuer)) { 445 continue; 446 } 447 String prefCommand = value.prefSubCommand; 448 449 final String[] psplit = ACFPatterns.SPACE.split(prefCommand); 450 cmds.add(psplit[args.length - 1]); 451 } 452 } 453 454 return filterTabComplete(args[args.length - 1], cmds); 455 } finally { 456 postCommandOperation(); 457 } 458 } 459 460 private List<String> completeCommand(CommandOperationContext commandOperationContext, CommandIssuer issuer, RegisteredCommand cmd, String[] args, String commandLabel) { 461 if (!cmd.hasPermission(issuer) || args.length > cmd.requiredResolvers + cmd.optionalResolvers || args.length == 0 462 || cmd.complete == null) { 463 return ImmutableList.of(); 464 } 465 466 String[] completions = ACFPatterns.SPACE.split(cmd.complete); 467 468 List<String> cmds = manager.getCommandCompletions().of(commandOperationContext, cmd, issuer, completions, args); 469 return filterTabComplete(args[args.length-1], cmds); 470 } 471 472 private static List<String> filterTabComplete(String arg, List<String> cmds) { 473 return cmds.stream() 474 .distinct() 475 .filter(cmd -> cmd != null && (arg.isEmpty() || ApacheCommonsLangUtil.startsWithIgnoreCase(cmd, arg))) 476 .collect(Collectors.toList()); 477 } 478 479 480 private boolean executeSubcommand(CommandOperationContext commandContext, String subcommand, CommandIssuer issuer, String... args) { 481 final Set<RegisteredCommand> defs = subCommands.get(subcommand); 482 RegisteredCommand def = null; 483 if (!defs.isEmpty()) { 484 if (defs.size() == 1) { 485 def = defs.iterator().next(); 486 } 487 if (def != null) { 488 executeCommand(commandContext, issuer, args, def); 489 return true; 490 } 491 } 492 return false; 493 } 494 495 private boolean checkPrecommand(CommandOperationContext commandOperationContext, RegisteredCommand cmd, CommandIssuer issuer, String[] args) { 496 Method pre = this.preCommandHandler; 497 if (pre != null) { 498 try { 499 Class<?>[] types = pre.getParameterTypes(); 500 Object[] parameters = new Object[pre.getParameterCount()]; 501 for (int i = 0; i < parameters.length; i++) { 502 Class<?> type = types[i]; 503 Object issuerObject = issuer.getIssuer(); 504 if (manager.isCommandIssuer(type) && type.isAssignableFrom(issuerObject.getClass())) { 505 parameters[i] = issuerObject; 506 } else if (CommandIssuer.class.isAssignableFrom(type)) { 507 parameters[i] = issuer; 508 } else if (RegisteredCommand.class.isAssignableFrom(type)) { 509 parameters[i] = cmd; 510 } else if (String[].class.isAssignableFrom((type))) { 511 parameters[i] = args; 512 } else { 513 parameters[i] = null; 514 } 515 } 516 517 return (boolean) pre.invoke(this, parameters); 518 } catch (IllegalAccessException | InvocationTargetException e) { 519 this.manager.log(LogLevel.ERROR, "Exception encountered while command pre-processing", e); 520 } 521 } 522 return false; 523 } 524 525 /** @deprecated Unstable API */ @Deprecated @UnstableAPI 526 public CommandHelp getCommandHelp() { 527 return manager.generateCommandHelp(); 528 } 529 530 /** @deprecated Unstable API */ @Deprecated @UnstableAPI 531 public void showCommandHelp() { 532 getCommandHelp().showHelp(); 533 } 534 535 public void help(Object issuer, String[] args) { 536 help(manager.getCommandIssuer(issuer), args); 537 } 538 public void help(CommandIssuer issuer, String[] args) { 539 issuer.sendMessage(MessageType.ERROR, MessageKeys.UNKNOWN_COMMAND); 540 } 541 public void doHelp(Object issuer, String... args) { 542 doHelp(manager.getCommandIssuer(issuer), args); 543 } 544 public void doHelp(CommandIssuer issuer, String... args) { 545 help(issuer, args); 546 } 547 548 public void showSyntax(CommandIssuer issuer, RegisteredCommand<?> cmd) { 549 issuer.sendMessage(MessageType.SYNTAX, MessageKeys.INVALID_SYNTAX, 550 "{command}", "/" + cmd.command, 551 "{syntax}", cmd.syntaxText 552 ); 553 } 554 555 public boolean hasPermission(Object issuer) { 556 return hasPermission(manager.getCommandIssuer(issuer)); 557 } 558 559 public boolean hasPermission(CommandIssuer issuer) { 560 return permission == null || permission.isEmpty() || (manager.hasPermission(issuer, permission) && (parentCommand == null || parentCommand.hasPermission(issuer))); 561 } 562 563 public String getName() { 564 return commandName; 565 } 566 567 public ExceptionHandler getExceptionHandler() { 568 return exceptionHandler; 569 } 570 571 public BaseCommand setExceptionHandler(ExceptionHandler exceptionHandler) { 572 this.exceptionHandler = exceptionHandler; 573 return this; 574 } 575 576 private static class CommandSearch { RegisteredCommand cmd; int argIndex; String checkSub; 577 578 CommandSearch(RegisteredCommand cmd, int argIndex, String checkSub) { 579 this.cmd = cmd; 580 this.argIndex = argIndex; 581 this.checkSub = checkSub; 582 } 583 584 String getCheckSub() { 585 return this.checkSub; 586 } 587 588 @Override 589 public boolean equals(Object o) { 590 if (this == o) return true; 591 if (o == null || getClass() != o.getClass()) return false; 592 CommandSearch that = (CommandSearch) o; 593 return argIndex == that.argIndex && 594 Objects.equals(cmd, that.cmd) && 595 Objects.equals(checkSub, that.checkSub); 596 } 597 598 @Override 599 public int hashCode() { 600 return Objects.hash(cmd, argIndex, checkSub); 601 } 602 } 603}