PageRenderTime 354ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

/worldedit-core/src/main/java/com/sk89q/minecraft/util/commands/CommandsManager.java

https://gitlab.com/Skull3x/WorldEdit
Java | 591 lines | 289 code | 87 blank | 215 comment | 67 complexity | 34066d693884cb5c3461988c5b9181f4 MD5 | raw file
  1. /*
  2. * WorldEdit, a Minecraft world manipulation toolkit
  3. * Copyright (C) sk89q <http://www.sk89q.com>
  4. * Copyright (C) WorldEdit team and contributors
  5. *
  6. * This program is free software: you can redistribute it and/or modify it
  7. * under the terms of the GNU Lesser General Public License as published by the
  8. * Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful, but WITHOUT
  12. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13. * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
  14. * for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. package com.sk89q.minecraft.util.commands;
  20. import com.sk89q.util.StringUtil;
  21. import java.lang.reflect.InvocationTargetException;
  22. import java.lang.reflect.Method;
  23. import java.lang.reflect.Modifier;
  24. import java.util.ArrayList;
  25. import java.util.HashMap;
  26. import java.util.HashSet;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Set;
  30. import java.util.logging.Level;
  31. import java.util.logging.Logger;
  32. /**
  33. * Manager for handling commands. This allows you to easily process commands,
  34. * including nested commands, by correctly annotating methods of a class.
  35. *
  36. * <p>To use this, it is merely a matter of registering classes containing
  37. * the commands (as methods with the proper annotations) with the
  38. * manager. When you want to process a command, use one of the
  39. * {@code execute} methods. If something is wrong, such as incorrect
  40. * usage, insufficient permissions, or a missing command altogether, an
  41. * exception will be raised for upstream handling.</p>
  42. *
  43. * <p>Methods of a class to be registered can be static, but if an injector
  44. * is registered with the class, the instances of the command classes
  45. * will be created automatically and methods will be called non-statically.</p>
  46. *
  47. * <p>To mark a method as a command, use {@link Command}. For nested commands,
  48. * see {@link NestedCommand}. To handle permissions, use
  49. * {@link CommandPermissions}.</p>
  50. *
  51. * <p>This uses Java reflection extensively, but to reduce the overhead of
  52. * reflection, command lookups are completely cached on registration. This
  53. * allows for fast command handling. Method invocation still has to be done
  54. * with reflection, but this is quite fast in that of itself.</p>
  55. *
  56. * @param <T> command sender class
  57. */
  58. @SuppressWarnings("ProtectedField")
  59. public abstract class CommandsManager<T> {
  60. protected static final Logger logger =
  61. Logger.getLogger(CommandsManager.class.getCanonicalName());
  62. /**
  63. * Mapping of commands (including aliases) with a description. Root
  64. * commands are stored under a key of null, whereas child commands are
  65. * cached under their respective {@link Method}. The child map has
  66. * the key of the command name (one for each alias) with the
  67. * method.
  68. */
  69. protected Map<Method, Map<String, Method>> commands = new HashMap<Method, Map<String, Method>>();
  70. /**
  71. * Used to store the instances associated with a method.
  72. */
  73. protected Map<Method, Object> instances = new HashMap<Method, Object>();
  74. /**
  75. * Mapping of commands (not including aliases) with a description. This
  76. * is only for top level commands.
  77. */
  78. protected Map<String, String> descs = new HashMap<String, String>();
  79. /**
  80. * Stores the injector used to getInstance.
  81. */
  82. protected Injector injector;
  83. /**
  84. * Mapping of commands (not including aliases) with a description. This
  85. * is only for top level commands.
  86. */
  87. protected Map<String, String> helpMessages = new HashMap<String, String>();
  88. /**
  89. * Register an class that contains commands (denoted by {@link Command}.
  90. * If no dependency injector is specified, then the methods of the
  91. * class will be registered to be called statically. Otherwise, new
  92. * instances will be created of the command classes and methods will
  93. * not be called statically.
  94. *
  95. * @param cls the class to register
  96. */
  97. public void register(Class<?> cls) {
  98. registerMethods(cls, null);
  99. }
  100. /**
  101. * Register an class that contains commands (denoted by {@link Command}.
  102. * If no dependency injector is specified, then the methods of the
  103. * class will be registered to be called statically. Otherwise, new
  104. * instances will be created of the command classes and methods will
  105. * not be called statically. A List of {@link Command} annotations from
  106. * registered commands is returned.
  107. *
  108. * @param cls the class to register
  109. * @return A List of {@link Command} annotations from registered commands,
  110. * for use in eg. a dynamic command registration system.
  111. */
  112. public List<Command> registerAndReturn(Class<?> cls) {
  113. return registerMethods(cls, null);
  114. }
  115. /**
  116. * Register the methods of a class. This will automatically construct
  117. * instances as necessary.
  118. *
  119. * @param cls the class to register
  120. * @param parent the parent method
  121. * @return Commands Registered
  122. */
  123. public List<Command> registerMethods(Class<?> cls, Method parent) {
  124. try {
  125. if (getInjector() == null) {
  126. return registerMethods(cls, parent, null);
  127. } else {
  128. Object obj = getInjector().getInstance(cls);
  129. return registerMethods(cls, parent, obj);
  130. }
  131. } catch (InvocationTargetException e) {
  132. logger.log(Level.SEVERE, "Failed to register commands", e);
  133. } catch (IllegalAccessException e) {
  134. logger.log(Level.SEVERE, "Failed to register commands", e);
  135. } catch (InstantiationException e) {
  136. logger.log(Level.SEVERE, "Failed to register commands", e);
  137. }
  138. return null;
  139. }
  140. /**
  141. * Register the methods of a class.
  142. *
  143. * @param cls the class to register
  144. * @param parent the parent method
  145. * @param obj the object whose methods will become commands if they are annotated
  146. * @return a list of commands
  147. */
  148. private List<Command> registerMethods(Class<?> cls, Method parent, Object obj) {
  149. Map<String, Method> map;
  150. List<Command> registered = new ArrayList<Command>();
  151. // Make a new hash map to cache the commands for this class
  152. // as looking up methods via reflection is fairly slow
  153. if (commands.containsKey(parent)) {
  154. map = commands.get(parent);
  155. } else {
  156. map = new HashMap<String, Method>();
  157. commands.put(parent, map);
  158. }
  159. for (Method method : cls.getMethods()) {
  160. if (!method.isAnnotationPresent(Command.class)) {
  161. continue;
  162. }
  163. boolean isStatic = Modifier.isStatic(method.getModifiers());
  164. Command cmd = method.getAnnotation(Command.class);
  165. // Cache the aliases too
  166. for (String alias : cmd.aliases()) {
  167. map.put(alias, method);
  168. }
  169. // We want to be able invoke with an instance
  170. if (!isStatic) {
  171. // Can't register this command if we don't have an instance
  172. if (obj == null) {
  173. continue;
  174. }
  175. instances.put(method, obj);
  176. }
  177. // Build a list of commands and their usage details, at least for
  178. // root level commands
  179. if (parent == null) {
  180. final String commandName = cmd.aliases()[0];
  181. final String desc = cmd.desc();
  182. final String usage = cmd.usage();
  183. if (usage.isEmpty()) {
  184. descs.put(commandName, desc);
  185. } else {
  186. descs.put(commandName, usage + " - " + desc);
  187. }
  188. String help = cmd.help();
  189. if (help.isEmpty()) {
  190. help = desc;
  191. }
  192. final CharSequence arguments = getArguments(cmd);
  193. for (String alias : cmd.aliases()) {
  194. final String helpMessage = "/" + alias + " " + arguments + "\n\n" + help;
  195. final String key = alias.replaceAll("/", "");
  196. String previous = helpMessages.put(key, helpMessage);
  197. if (previous != null && !previous.replaceAll("^/[^ ]+ ", "").equals(helpMessage.replaceAll("^/[^ ]+ ", ""))) {
  198. helpMessages.put(key, previous + "\n\n" + helpMessage);
  199. }
  200. }
  201. }
  202. // Add the command to the registered command list for return
  203. registered.add(cmd);
  204. // Look for nested commands -- if there are any, those have
  205. // to be cached too so that they can be quickly looked
  206. // up when processing commands
  207. if (method.isAnnotationPresent(NestedCommand.class)) {
  208. NestedCommand nestedCmd = method.getAnnotation(NestedCommand.class);
  209. for (Class<?> nestedCls : nestedCmd.value()) {
  210. registerMethods(nestedCls, method);
  211. }
  212. }
  213. }
  214. if (cls.getSuperclass() != null) {
  215. registerMethods(cls.getSuperclass(), parent, obj);
  216. }
  217. return registered;
  218. }
  219. /**
  220. * Checks to see whether there is a command named such at the root level.
  221. * This will check aliases as well.
  222. *
  223. * @param command the command
  224. * @return true if the command exists
  225. */
  226. public boolean hasCommand(String command) {
  227. return commands.get(null).containsKey(command.toLowerCase());
  228. }
  229. /**
  230. * Get a list of command descriptions. This is only for root commands.
  231. *
  232. * @return a map of commands
  233. */
  234. public Map<String, String> getCommands() {
  235. return descs;
  236. }
  237. /**
  238. * Get the mapping of methods under a parent command.
  239. *
  240. * @return the mapping
  241. */
  242. public Map<Method, Map<String, Method>> getMethods() {
  243. return commands;
  244. }
  245. /**
  246. * Get a map from command name to help message. This is only for root commands.
  247. *
  248. * @return a map of help messages for each command
  249. */
  250. public Map<String, String> getHelpMessages() {
  251. return helpMessages;
  252. }
  253. /**
  254. * Get the usage string for a command.
  255. *
  256. * @param args the arguments
  257. * @param level the depth of the command
  258. * @param cmd the command annotation
  259. * @return the usage string
  260. */
  261. protected String getUsage(String[] args, int level, Command cmd) {
  262. final StringBuilder command = new StringBuilder();
  263. command.append('/');
  264. for (int i = 0; i <= level; ++i) {
  265. command.append(args[i]);
  266. command.append(' ');
  267. }
  268. command.append(getArguments(cmd));
  269. final String help = cmd.help();
  270. if (!help.isEmpty()) {
  271. command.append("\n\n");
  272. command.append(help);
  273. }
  274. return command.toString();
  275. }
  276. protected CharSequence getArguments(Command cmd) {
  277. final String flags = cmd.flags();
  278. final StringBuilder command2 = new StringBuilder();
  279. if (!flags.isEmpty()) {
  280. String flagString = flags.replaceAll(".:", "");
  281. if (!flagString.isEmpty()) {
  282. command2.append("[-");
  283. for (int i = 0; i < flagString.length(); ++i) {
  284. command2.append(flagString.charAt(i));
  285. }
  286. command2.append("] ");
  287. }
  288. }
  289. command2.append(cmd.usage());
  290. return command2;
  291. }
  292. /**
  293. * Get the usage string for a nested command.
  294. *
  295. * @param args the arguments
  296. * @param level the depth of the command
  297. * @param method the parent method
  298. * @param player the player
  299. * @return the usage string
  300. * @throws CommandException on some error
  301. */
  302. protected String getNestedUsage(String[] args, int level, Method method, T player) throws CommandException {
  303. StringBuilder command = new StringBuilder();
  304. command.append("/");
  305. for (int i = 0; i <= level; ++i) {
  306. command.append(args[i]).append(" ");
  307. }
  308. Map<String, Method> map = commands.get(method);
  309. boolean found = false;
  310. command.append("<");
  311. Set<String> allowedCommands = new HashSet<String>();
  312. for (Map.Entry<String, Method> entry : map.entrySet()) {
  313. Method childMethod = entry.getValue();
  314. found = true;
  315. if (hasPermission(childMethod, player)) {
  316. Command childCmd = childMethod.getAnnotation(Command.class);
  317. allowedCommands.add(childCmd.aliases()[0]);
  318. }
  319. }
  320. if (!allowedCommands.isEmpty()) {
  321. command.append(StringUtil.joinString(allowedCommands, "|", 0));
  322. } else {
  323. if (!found) {
  324. command.append("?");
  325. } else {
  326. //command.append("action");
  327. throw new CommandPermissionsException();
  328. }
  329. }
  330. command.append(">");
  331. return command.toString();
  332. }
  333. /**
  334. * Attempt to execute a command. This version takes a separate command
  335. * name (for the root command) and then a list of following arguments.
  336. *
  337. * @param cmd command to run
  338. * @param args arguments
  339. * @param player command source
  340. * @param methodArgs method arguments
  341. * @throws CommandException thrown when the command throws an error
  342. */
  343. public void execute(String cmd, String[] args, T player, Object... methodArgs) throws CommandException {
  344. String[] newArgs = new String[args.length + 1];
  345. System.arraycopy(args, 0, newArgs, 1, args.length);
  346. newArgs[0] = cmd;
  347. Object[] newMethodArgs = new Object[methodArgs.length + 1];
  348. System.arraycopy(methodArgs, 0, newMethodArgs, 1, methodArgs.length);
  349. executeMethod(null, newArgs, player, newMethodArgs, 0);
  350. }
  351. /**
  352. * Attempt to execute a command.
  353. *
  354. * @param args the arguments
  355. * @param player the player
  356. * @param methodArgs the arguments for the method
  357. * @throws CommandException thrown on command error
  358. */
  359. public void execute(String[] args, T player, Object... methodArgs) throws CommandException {
  360. Object[] newMethodArgs = new Object[methodArgs.length + 1];
  361. System.arraycopy(methodArgs, 0, newMethodArgs, 1, methodArgs.length);
  362. executeMethod(null, args, player, newMethodArgs, 0);
  363. }
  364. /**
  365. * Attempt to execute a command.
  366. *
  367. * @param parent the parent method
  368. * @param args an array of arguments
  369. * @param player the player
  370. * @param methodArgs the array of method arguments
  371. * @param level the depth of the command
  372. * @throws CommandException thrown on a command error
  373. */
  374. public void executeMethod(Method parent, String[] args, T player, Object[] methodArgs, int level) throws CommandException {
  375. String cmdName = args[level];
  376. Map<String, Method> map = commands.get(parent);
  377. Method method = map.get(cmdName.toLowerCase());
  378. if (method == null) {
  379. if (parent == null) { // Root
  380. throw new UnhandledCommandException();
  381. } else {
  382. throw new MissingNestedCommandException("Unknown command: " + cmdName,
  383. getNestedUsage(args, level - 1, parent, player));
  384. }
  385. }
  386. checkPermission(player, method);
  387. int argsCount = args.length - 1 - level;
  388. // checks if we need to execute the body of the nested command method (false)
  389. // or display the help what commands are available (true)
  390. // this is all for an args count of 0 if it is > 0 and a NestedCommand Annotation is present
  391. // it will always handle the methods that NestedCommand points to
  392. // e.g.:
  393. // - /cmd - @NestedCommand(executeBody = true) will go into the else loop and execute code in that method
  394. // - /cmd <arg1> <arg2> - @NestedCommand(executeBody = true) will always go to the nested command class
  395. // - /cmd <arg1> - @NestedCommand(executeBody = false) will always go to the nested command class not matter the args
  396. boolean executeNested = method.isAnnotationPresent(NestedCommand.class)
  397. && (argsCount > 0 || !method.getAnnotation(NestedCommand.class).executeBody());
  398. if (executeNested) {
  399. if (argsCount == 0) {
  400. throw new MissingNestedCommandException("Sub-command required.",
  401. getNestedUsage(args, level, method, player));
  402. } else {
  403. executeMethod(method, args, player, methodArgs, level + 1);
  404. }
  405. } else if (method.isAnnotationPresent(CommandAlias.class)) {
  406. CommandAlias aCmd = method.getAnnotation(CommandAlias.class);
  407. executeMethod(parent, aCmd.value(), player, methodArgs, level);
  408. } else {
  409. Command cmd = method.getAnnotation(Command.class);
  410. String[] newArgs = new String[args.length - level];
  411. System.arraycopy(args, level, newArgs, 0, args.length - level);
  412. final Set<Character> valueFlags = new HashSet<Character>();
  413. char[] flags = cmd.flags().toCharArray();
  414. Set<Character> newFlags = new HashSet<Character>();
  415. for (int i = 0; i < flags.length; ++i) {
  416. if (flags.length > i + 1 && flags[i + 1] == ':') {
  417. valueFlags.add(flags[i]);
  418. ++i;
  419. }
  420. newFlags.add(flags[i]);
  421. }
  422. CommandContext context = new CommandContext(newArgs, valueFlags);
  423. if (context.argsLength() < cmd.min()) {
  424. throw new CommandUsageException("Too few arguments.", getUsage(args, level, cmd));
  425. }
  426. if (cmd.max() != -1 && context.argsLength() > cmd.max()) {
  427. throw new CommandUsageException("Too many arguments.", getUsage(args, level, cmd));
  428. }
  429. if (!cmd.anyFlags()) {
  430. for (char flag : context.getFlags()) {
  431. if (!newFlags.contains(flag)) {
  432. throw new CommandUsageException("Unknown flag: " + flag, getUsage(args, level, cmd));
  433. }
  434. }
  435. }
  436. methodArgs[0] = context;
  437. Object instance = instances.get(method);
  438. invokeMethod(parent, args, player, method, instance, methodArgs, argsCount);
  439. }
  440. }
  441. protected void checkPermission(T player, Method method) throws CommandException {
  442. if (!hasPermission(method, player)) {
  443. throw new CommandPermissionsException();
  444. }
  445. }
  446. public void invokeMethod(Method parent, String[] args, T player, Method method, Object instance, Object[] methodArgs, int level) throws CommandException {
  447. try {
  448. method.invoke(instance, methodArgs);
  449. } catch (IllegalArgumentException e) {
  450. logger.log(Level.SEVERE, "Failed to execute command", e);
  451. } catch (IllegalAccessException e) {
  452. logger.log(Level.SEVERE, "Failed to execute command", e);
  453. } catch (InvocationTargetException e) {
  454. if (e.getCause() instanceof CommandException) {
  455. throw (CommandException) e.getCause();
  456. }
  457. throw new WrappedCommandException(e.getCause());
  458. }
  459. }
  460. /**
  461. * Returns whether a player has access to a command.
  462. *
  463. * @param method the method
  464. * @param player the player
  465. * @return true if permission is granted
  466. */
  467. protected boolean hasPermission(Method method, T player) {
  468. CommandPermissions perms = method.getAnnotation(CommandPermissions.class);
  469. if (perms == null) {
  470. return true;
  471. }
  472. for (String perm : perms.value()) {
  473. if (hasPermission(player, perm)) {
  474. return true;
  475. }
  476. }
  477. return false;
  478. }
  479. /**
  480. * Returns whether a player permission..
  481. *
  482. * @param player the player
  483. * @param permission the permission
  484. * @return true if permission is granted
  485. */
  486. public abstract boolean hasPermission(T player, String permission);
  487. /**
  488. * Get the injector used to create new instances. This can be
  489. * null, in which case only classes will be registered statically.
  490. *
  491. * @return an injector instance
  492. */
  493. public Injector getInjector() {
  494. return injector;
  495. }
  496. /**
  497. * Set the injector for creating new instances.
  498. *
  499. * @param injector injector or null
  500. */
  501. public void setInjector(Injector injector) {
  502. this.injector = injector;
  503. }
  504. }