CommandController and Method/Annotation based Command delegation

Discussion in 'Resources' started by AmoebaMan, Dec 16, 2012.

Thread Status:
Not open for further replies.
  1. Offline

    AmoebaMan

    So as we all know, a LONG time ago Bukkit swapped it's old Event handling system for a more flexible one, based off of runtime registration of methods through the use of the @EventHandler annotation. Personally, I loved this system. It was more flexible, nicer to write, and didn't require you to create a whole new class if you only wanted to listen for a single event. You could even write a one-class plugin if you liked.

    However, what I still don't like and have never liked is Bukkit's way of handling commands. I dislike having to register commands and ALL of their attributes in the plugin YAML file. I find it unwieldy, and I've seen a lot of new programmers confused by it. Furthermore, I don't like the onCommand system. I'm not sure why, but I just don't. Perhaps its because I can't minimize individual commands in Eclipse unless I write entirely new classes to handle every single command.

    So what I've done is written a small class that you can chuck into any plugin to circumvent this. When the plugin enables, call the registerCommands method with the plugin and object to register for (EXACTLY the way that you call the registerEvents method for events), and that's all. Beyond that, all you'll need to do is make sure that you've set up the handler class properly, but again, it is extremely similar to the way that event listeners are set up.

    Here's the actual class:
    Code:java
    1. package com.amoebaman.kitmaster.utilities;
    2.  
    3. import java.lang.annotation.Retention;
    4. import java.lang.annotation.RetentionPolicy;
    5. import java.lang.reflect.Method;
    6. import java.util.HashMap;
    7.  
    8. import org.bukkit.ChatColor;
    9. import org.bukkit.command.Command;
    10. import org.bukkit.command.CommandExecutor;
    11. import org.bukkit.command.CommandSender;
    12. import org.bukkit.command.ConsoleCommandSender;
    13. import org.bukkit.entity.Player;
    14. import org.bukkit.plugin.java.JavaPlugin;
    15.  
    16. import com.google.common.collect.Lists;
    17.  
    18. public class CommandController implements CommandExecutor{
    19.  
    20. private final static HashMap<Command, Object> handlers = new HashMap<Command, Object>();
    21. private final static HashMap<Command, Method> methods = new HashMap<Command, Method>();
    22. private final static HashMap<String, SubCommand> subCommands = new HashMap<String, SubCommand>();
    23. private final static HashMap<String, Object> subHandlers = new HashMap<String, Object>();
    24. private final static HashMap<String, Method> subMethods = new HashMap<String, Method>();
    25.  
    26. /**
    27.   * Registers all command handlers and subcommand handlers in a class, matching them with their corresponding commands and subcommands registered to the specified plugin.
    28.   * @param plugin The plugin whose commands will be considered for registration
    29.   * @param handler An instance of the class whose methods will be considered for registration
    30.   */
    31. public static void registerCommands(JavaPlugin plugin, Object handler){
    32.  
    33. for(Method method : handler.getClass().getMethods()){
    34. Class<?>[] params = method.getParameterTypes();
    35. if(params.length == 2 && CommandSender.class.isAssignableFrom(params[0]) && String[].class.equals(params[1])){
    36.  
    37. if(isCommandHandler(method)){
    38. CommandHandler annotation = method.getAnnotation(CommandHandler.class);
    39. if(plugin.getCommand(annotation.name()) != null){
    40. plugin.getCommand(annotation.name()).setExecutor(new CommandController());
    41. if(!(annotation.aliases().equals(new String[]{""})))
    42. plugin.getCommand(annotation.name()).setAliases(Lists.newArrayList(annotation.aliases()));
    43. if(!annotation.description().equals(""))
    44. plugin.getCommand(annotation.name()).setDescription(annotation.description());
    45. if(!annotation.usage().equals(""))
    46. plugin.getCommand(annotation.name()).setUsage(annotation.usage());
    47. if(!annotation.permission().equals(""))
    48. plugin.getCommand(annotation.name()).setPermission(annotation.permission());
    49. if(!annotation.permissionMessage().equals(""))
    50. plugin.getCommand(annotation.name()).setPermissionMessage(ChatColor.RED + annotation.permissionMessage());
    51. handlers.put(plugin.getCommand(annotation.name()), handler);
    52. methods.put(plugin.getCommand(annotation.name()), method);
    53. }
    54. }
    55.  
    56. if(isSubCommandHandler(method)){
    57. SubCommandHandler annotation = method.getAnnotation(SubCommandHandler.class);
    58. if(plugin.getCommand(annotation.parent()) != null){
    59. plugin.getCommand(annotation.parent()).setExecutor(new CommandController());
    60. SubCommand subCommand = new SubCommand(plugin.getCommand(annotation.parent()), annotation.name());
    61. subCommand.permission = annotation.permission();
    62. subCommand.permissionMessage = annotation.permissionMessage();
    63. subCommands.put(subCommand.toString(), subCommand);
    64. subHandlers.put(subCommand.toString(), handler);
    65. subMethods.put(subCommand.toString(), method);
    66. }
    67. }
    68. }
    69. }
    70. }
    71.  
    72. /**
    73.   * @author AmoebaMan
    74.   * An annotation interface that may be attached to a method to designate it as a command handler.
    75.   * When registering a handler with this class, only methods marked with this annotation will be considered for command registration.
    76.   */
    77. @Retention(RetentionPolicy.RUNTIME)
    78. public static @interface CommandHandler {
    79. String name();
    80. String[] aliases() default { "" };
    81. String description() default "";
    82. String usage() default "";
    83. String permission() default "";
    84. String permissionMessage() default "You do not have permission to use that command";
    85. }
    86.  
    87. /**
    88.   * Tests if a method is a command handler
    89.   */
    90. private static boolean isCommandHandler(Method method){
    91. return method.getAnnotation(CommandHandler.class) != null;
    92. }
    93.  
    94. /**
    95.   * @author AmoebaMan
    96.   * An annotation interface that may be attached to a method to designate it as a subcommand handler.
    97.   * When registering a handler with this class, only methods marked with this annotation will be considered for subcommand registration.
    98.   */
    99. @Retention(RetentionPolicy.RUNTIME)
    100. public static @interface SubCommandHandler {
    101. String parent();
    102. String name();
    103. String permission() default "";
    104. String permissionMessage() default "You do not have permission to use that command";
    105. }
    106.  
    107. /**
    108.   * Tests if a method is a subcommand handler
    109.   */
    110. private static boolean isSubCommandHandler(Method method){
    111. return method.getAnnotation(SubCommandHandler.class) != null;
    112. }
    113.  
    114. /**
    115.   * A class for representing subcommands
    116.   */
    117. private static class SubCommand{
    118. public final Command superCommand;
    119. public final String subCommand;
    120. public String permission;
    121. public String permissionMessage;
    122. public SubCommand(Command superCommand, String subCommand){
    123. this.superCommand = superCommand;
    124. this.subCommand = subCommand.toLowerCase();
    125. }
    126. public boolean equals(Object x){ return toString().equals(x.toString()); }
    127. public String toString(){ return (superCommand.getName() + " " + subCommand).trim(); }
    128. }
    129.  
    130. /**
    131.   * This is the method that "officially" processes commands, but in reality it will always delegate responsibility to the handlers and methods assigned to the command or subcommand
    132.   * Beyond checking permissions, checking player/console sending, and invoking handlers and methods, this method does not actually act on the commands
    133.   */
    134. public boolean onCommand(CommandSender sender, Command command, String label, String[] args){
    135. /*
    136.   * If a subcommand may be present...
    137.   */
    138. if(args.length > 0){
    139. /*
    140.   * Get the subcommand given and the handler and method attached to it
    141.   */
    142. SubCommand subCommand = new SubCommand(command, args[0]);
    143. subCommand = subCommands.get(subCommand.toString());
    144. /*
    145.   * If and only if the subcommand actually exists...
    146.   */
    147. if(subCommand != null){
    148. Object subHandler = subHandlers.get(subCommand.toString());
    149. Method subMethod = subMethods.get(subCommand.toString());
    150. /*
    151.   * If and only if both handler and method exist...
    152.   */
    153. if(subHandler != null && subMethod != null){
    154. /*
    155.   * Reorder the arguments so we don't resend the subcommand
    156.   */
    157. String[] subArgs = new String[args.length - 1];
    158. for(int i = 1; i < args.length; i++)
    159. subArgs[i - 1] = args[i];
    160. /*
    161.   * If the method requires a player and the subcommand wasn't sent by one, don't continue
    162.   */
    163. if(subMethod.getParameterTypes()[0].equals(Player.class) && !(sender instanceof Player)){
    164. sender.sendMessage(ChatColor.RED + "This command requires a player sender");
    165. return true;
    166. }
    167. /*
    168.   * If the method requires a console and the subcommand wasn't sent by one, don't continue
    169.   */
    170. if(subMethod.getParameterTypes()[0].equals(ConsoleCommandSender.class) && !(sender instanceof ConsoleCommandSender)){
    171. sender.sendMessage(ChatColor.RED + "This command requires a console sender");
    172. return true;
    173. }
    174. /*
    175.   * If a permission is attached to this subcommand and the sender doens't have it, don't continue
    176.   */
    177. if(!subCommand.permission.isEmpty() && !sender.hasPermission(subCommand.permission)){
    178. sender.sendMessage(ChatColor.RED + subCommand.permissionMessage);
    179. return true;
    180. }
    181. /*
    182.   * Try to process the command
    183.   */
    184. try{ subMethod.invoke(subHandler, sender, args); }
    185. catch(Exception e){
    186. sender.sendMessage(ChatColor.RED + "An error occurred while trying to process the command");
    187. e.printStackTrace();
    188. }
    189. return true;
    190. }
    191. }
    192. }
    193. /*
    194.   * If a subcommand was successfully executed, the command will not reach this point
    195.   * Get the handler and method attached to this command
    196.   */
    197. Object handler = handlers.get(command);
    198. Method method = methods.get(command);
    199. /*
    200.   * If and only if both handler and method exist...
    201.   */
    202. if(handler != null && method != null){
    203. /*
    204.   * If the method requires a player and the command wasn't sent by one, don't continue
    205.   */
    206. if(method.getParameterTypes()[0].equals(Player.class) && !(sender instanceof Player)){
    207. sender.sendMessage(ChatColor.RED + "This command requires a player sender");
    208. return true;
    209. }
    210. /*
    211.   * If the method requires a console and the command wasn't sent by one, don't continue
    212.   */
    213. if(method.getParameterTypes()[0].equals(ConsoleCommandSender.class) && !(sender instanceof ConsoleCommandSender)){
    214. sender.sendMessage(ChatColor.RED + "This command requires a console sender");
    215. return true;
    216. }
    217. /*
    218.   * Try to process the command
    219.   */
    220. try{ method.invoke(handler, sender, args); }
    221. catch(Exception e){
    222. sender.sendMessage(ChatColor.RED + "An error occurred while trying to process the command");
    223. e.printStackTrace();
    224. }
    225. }
    226. /*
    227.   * Otherwise we have to fake not recognising the command
    228.   */
    229. else
    230. sender.sendMessage("Unknown command. Type \"help\" for help.");
    231.  
    232. return true;
    233. }
    234.  
    235. }[I][/I][/i]


    Here's the basic usage of this class:
    1. Create a class (any class, no extension or implementation requirements) to serve as your command handler.
    2. Write methods to handle commands. Just as well EventHandlers, the names do not matter. The parameters, however do. The first parameter must be some subclass of CommandSender (typically either CommandSender or Player, but you can also specify ConsoleCommandSender), and the second parameter must be a String[] (String array).
    3. Add the @CommandHandler annotation. You must include the name of the command in this. Optionally, you can add all the additional fields that can be defined in the plugin YAML. Aliases however will NOT work, as the method for setting them in Bukkit is broken (and will not be fixed).
    4. Register the handler with its plugin using the command. Example is below.
    Here's a sample handler class and method to handle a command:
    Code:java
    1. public class CommandClass{
    Code:java
    1.  
    2. [I] @CommandHandler( name = "setspawn" )[/I]
    3. [I] public void setSpawnCommand(Player player, String[] args){[/I]
    4. [I] //code to set the spawn location[/I]
    5. [I] }[/I]
    6. [I]}[/I]

    And here's the code to register that handler class and method to its command:
    Code:java
    1. CommandController.registerCommands(plugin, new CommandClass());

    Here are a few features that this system includes, built in:
    1. Quick and easy discrimination about what can send which commands. If the first parameter of the method is a more specific subclass of CommandSender, only that type of sender will be allowed to execute the command, and any others will be given a notification when they try.
    2. Fewer arguments to deal with. Command handler is much neater and more compact.
    3. Never use the onCommand method again!
    4. If you use Eclipse you can now minimize individual commands, because they're each assigned their own method.
    In addition to all the wonderfulness, the CommandController can also register and handle sub-commands! These are included thoughtfully for those plugins who prefer to prefix all their functional commands with an alias of the plugin name (think plugins like AutoMessage with /automessage ... or WorldGuard with /region ...). They are registered and handled exactly the same way as normal commands, but in the annotation you'll have to include the parent function as well. Here's an example for you:
    Code:java
    1. public class SubCommandClass{
    Code:java
    1.  
    2. [I] @SubCommandHandler(parent = "region", name = "define")[/I]
    3. [I] public void regionDefineCommand(Player player, String[] args){[/I]
    4. [I] //code to define a region[/I]
    5. [I] }[/I]
    6. [I] @SubCommandHandler(parent = "region", name = "info")[/I]
    7. [I] public void regionInfoCommand(Player player, String[] args){[/I]
    8. [I] //code to get info about a region[/I]
    9. [I] }[/I]
    10. [I]}[/I]

    Things to remember about using subcommands:
    1. The argument that is actually used to define the subcommand will be automatically removed from the argument list being sent in the String[]. The argument list will only include all the arguments AFTER the subcommand.
    2. Subcommands will always override their parent commands. If a subcommand exists for a given command, and is called, the parent command will never be called, even if an error occurs.
    That's all for now folks, I hope this helps, and I hope I don't get bashed too much for overriding traditional Bukkit functionality!



    By the way, all of this is tested and works.


    I'm not sure why everything went all italic and junk, but oh well...

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 30, 2016
    20zinnm, Maulss, bobacadodl and 2 others like this.
  2. Offline

    NinjaW0lf

    Think u meant to put this in resources.
     
  3. Offline

    Gravity

    Moved to the appropriate section.
     
  4. Offline

    AmoebaMan

  5. Oh boy, i just love this:D
     
  6. Offline

    Junrall

    AmoebaMan

    This has made things so much easier :p

    I see that your CommandController class can handle permission nodes. How do I use this feature? And is usable with Vault?
     
  7. Offline

    unrealdesign

    Junrall

    Yes vault is compatible with it, I use it and it works wonders! :)


    AmoebaMan

    Before anything, thank you so much for making this, it is very helpful in a multitude ways.

    My question: How do I save to the config?
    I've been using the same way I always have but it doesn't seem to be working? I've tried different things over the span of 2 days, and I've kind of rage quit. Here is what I have so far:

    In my subCommand:
    Code:
    this.addPlayerBounty(args[2], args[1]);
    In my main class:
    Code:
    private HashMap<String, String> playerBounty;
     
    public void addPlayerBounty(final String newBounty, final String newPrice){
            this.playerBounty.put(newBounty, newPrice);
            //this.getConfig().set("bounties."+newBounty, newPrice);
            this.getConfig().set("bounties.djoutbased", "10");
            this.saveConfig();
        }
    Not sure what I'm doing wrong, as I've used it in other plugins without these problems. I've tried
    doing
    Code:
    this.plugin.addBounty(args[1], args[2]);
    but then it have to add a bunch of stuff and eventually change
    Code:
    CommandController.registerCommands(this, new Bounty());
    a bit which just leads to mayhem. I did try it, but still it didn't work.

    This may not be a problem with your code, but I was just asking for general advice overall, thanks ^.^
     
  8. Offline

    Junrall

    unrealdesign
    Very cool!

    Could you please show how vault is used with this?

    Thanks! :D
     
  9. Offline

    unrealdesign

  10. Offline

    Junrall

    unrealdesign
    I'm comfortable with the use of vault :)

    I was just curious about the references to permissions within CommandController. It looks like you can assign permission nodes through CommandController. If this is the case, how would pass permission nodes to it? Maybe like this:?

    Code:
    public class SubCommandClass{
        @SubCommandHandler(parent = "region", name = "define", permission = "region.define")
        public void regionDefineCommand(Player player, String[] args){
            //code to define a region
        }
        @SubCommandHandler(parent = "region", name = "info", permission = "region.info")
        public void regionInfoCommand(Player player, String[] args){
            //code to get info about a region
        }
    }
     
  11. Offline

    unrealdesign

    Not sure to be honest, you'd have to reference his code, or ask him when he responds. Also I fixed my own problem >.>
     
  12. Offline

    Junrall

    unrealdesign
    It appears that we both came here for answers and ended up solving our own problems. Lol
    My examples above are the correct way to have CommandController check permissions for a command.

    Here is what you can pass to it:
    parent = "region" The main command
    name = "define" The sub command.
    aliases = "rdefine, rd, regdef" Alternate aliases for the command. You have to separate the aliases with commas
    description = "Sets a cubed region." Description of the command
    usage = "/region define - Defines a cuboid."Show how to use the command
    permission = "region.define" The permission node to check for this command
    permissionMessage = "You do not have permission for this command." The message to display if the person does not have permission to use the command.
     
  13. Offline

    unrealdesign

    Junrall

    Well thanks for all that! Now I won't have to go through the code myself ^.^ You the best!

    /internethug
     
  14. Offline

    Wingzzz

    I am having some difficulties trying this out unfortunately. Seperate "public class TestCommand" with the method:
    Code:
    @CommandHandler(name = "test", description = "testing testing testing ", permission = "lf.test", permissionMessage = "Sorry, you lack the sufficient permissions to execute this.", usage = "Wrong syntax; try: /test")
    public void test(CommandSender cs, String[] args) {
      Messaging messaging = new Messaging(this.plugin);
      messaging.send(cs, ChatColor.GREEN + "executing!");
      messaging.logInfo(ChatColor.GREEN + "executing!");
    }
    I've assumed I am to register it inside the main onEnable()... like so:
    Code:
    CommandController.registerCommands(this, new TestCommand());
    
    Any insight would be greatly appreciated.
     
  15. Offline

    Comphenix

    Have you added the command to plugin.yml?
    Code:
    commands:
       test:
          description: Testing command.
    
     
  16. Offline

    Wingzzz

    Hah! My bad, just went from reflection so I recently haven't been using that, good catch!
     
  17. Offline

    AmoebaMan

    Yeah, plugin.yml is still required at the most basic level. I could probably hack into Bukkit's registration system and force it in if I wanted to, but it'd be messy, and I'd rather not risk breaking Bukkit entirely.
     
  18. Offline

    Wingzzz

    Hehe, I just have a need for dynamically named commands, so reflection is what I'm going with :)
     
  19. Offline

    totemo

    This appears to be exactly what I was looking for. Thanks so much.
     
  20. Offline

    xigsag

    This is indeed what i have been looking for, since I'm too lazy to actually code the interface myself. Though I have not personally gone through the process of making one like this, I cannot tell what is wrong, as I seem to be getting an error.

    CommandController Line 43-50,
    [ The method description() is undefined for CommandController.CommandHandler ]

    CommandController Line 81-84,
    [ This method requires a body instead of a semi-colon ]

    Not sure what it means. I've seen how annotations work, but don't get whats going wrong.

    A little help?
     
Thread Status:
Not open for further replies.

Share This Page