Wolfmans Howlings

A programmers Blog about Programming solutions and a few other issues

Using java enum for command dispatching

Posted by Jim Morris on Sun Apr 12 01:26:06 -0700 2009

I haven't blogged much about Java even though it is my primary programming language. Since I have been doing a lot of Java recently I thought I'd post something about some of the Java idioms I've used over the years.

One I have been using a lot recently is a command dispatcher where the command is text.

In some languages you would probably use a switch statement with the command string being the case statement. However Java does not allow strings as the match part of a case.

A common idiom seen is to use enums (in Java 5 and greater), as they are constants and can be used in a switch statement.

Now enums are very powerful constructs in Java, they are not simply convenient #define work-arounds, although they are commonly used that way...

    enum Days { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
            SATURDAY, SUNDAY};

    switch(day){
      case MONDAY: do something with monday;
      case TUESDAY: do something with tuesday;
    }

See this for all the info on enums.

However you can do so much more with enums...

I use them as a super duper switch statement to dispatch text commands, and the cool thing is they are pretty efficient as they use a hash table lookup to do the dispatch. Here's an example of a command line app that uses the java Gnu GetOpt package with long option names that dispatches different commands that can be typed on the command line...

> doit --froboz one two three
> doit --bazznot glozny frobod

where --froboz is a command that takes three parameters, you get the idea.

package com.e4net;
import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;

public class Test {
    private static void usage() {

        System.out.println("Usage: myapp [-x filename] [--command] [args...]");
        System.out.println("   where commands is one of:");
        System.out.println("   froboz param1 param2 param3");
        System.out.println("   bazznot param1 param2");
        System.exit(1);
    }

    // The various supported command line commands
    // code is the short form command argument
    // size is the number of arguments needed for this command
    private enum Commands {
        froboz('f', 3) {
            boolean doit(int off, String [] argv){
                // do whatever froboz does with three arguments
                System.out.println("In froboz with: " + argv[off] + ", " + argv[off+1] + ", " + argv[off+2]);
                return true;
            }
        },

        bazznot('b', 2) {
            boolean doit(int off, String [] argv){
                // do whatever bazznot does with two arguments
                System.out.println("In bazznot with: " + argv[off] + ", " + argv[off+1]);
                return true;
            }
        };

        // Just add more commands here....

        private final int code, size;
        Commands(int code, int size){
            this.code= code;
            this.size= size;
        }
        public int code() { return code; }
        public int size() { return size; }

        // Do the command, off is the offset within argv the commands
        // parameters start
        abstract boolean doit(int off, String [] argv);

    };

    public static void main(String[] args) {

        // build the long commands from the enum
        LongOpt[] longopts = new LongOpt[Commands.values().length+1];
        longopts[0] = new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h');

        // loops through each enum and generates the long command
        // based on the enums value, the code() is the short form of
        // the command so either --froboz can be used or -f
        int off= 1;
        for(Commands c : Commands.values()){
            longopts[off++] = new LongOpt(c.toString(), LongOpt.NO_ARGUMENT, null, c.code());
        }

        Getopt g = new Getopt("myprog", args, "x:", longopts);
        String file= "default.file";
        int c;
        boolean found= false;
        while ((c = g.getopt()) != -1 && !found) {
            int i= g.getOptind();
            switch (c) {
                case 'x': // get some file name with the -x
                    // option
                    file = g.getOptarg();
                    break;

                    // this is the help which runs with --help or -h
                case '?':
                case 'h':
                    usage();
                    break;

                // this will lookup the command and execute it
                default:
                    // see if in commands
                    String cmd= longopts[g.getLongind()].getName();
                    Commands cm= null;
                    try {
                        // lookup the text command in the enum
                        cm= Commands.valueOf(cmd);                      
                    } catch (IllegalArgumentException e) {
                        usage();
                    }

                    // check we have enough arguments for this command
                    if(args.length - i == cm.size){
                        // do the command
                        cm.doit(g.getOptind(), args);
                        found= true;
                    }else {
                        // not enough arguments so show usage
                        System.err.println("Not enough arguments for: " + cmd);
                        usage();
                    }
            }
        }
    }
}

Ok that looks like a lot of code for a command lookup/dispatcher, but the nice thing about it is it is fairly DRY, to add a new command you simply add another enum term, the rest is taken care of automatically.

The command is looked up in the switches default clause using the valueOf method of an enum which basically finds the enum that matches the given string.

It can be run thusly...

> java -cp ./java-getopt.jar:./classes com.e4net.Test --bazznot 1 2
In bazznot with: 1, 2

> java -cp ./java-getopt.jar:./classes com.e4net.Test --froboz 1 2 3
In froboz with: 1, 2, 3


> java -cp ./java-getopt.jar:./classes com.e4net.Test --blahblah 1 2 3
myprog: unrecognized option '--blahblah'
Usage: myapp [-x filename] [--command] [args...]
    where commands is one of:
    froboz param1 param2 param3
    bazznot param1 param2

A variation on this theme is if say you have an Internet server, that receives text commands over a socket, we want to lookup the command we received and dispatch the same way as we do above. This turns out to be easy, we use the valueOf method of an enum. Below we call handle() with the command we got over the wire, and it is neatly dispatched. valueOf() is the command that will do the lookup in a hash table, so is quite efficient if the command list is long.

private Commands getCommand(String command) {
    Commands cmd;
    try {
        cmd= Commands.valueOf(command.toLowerCase());
    } catch (IllegalArgumentException e) {
        cmd= null;          
    }
    return cmd;
}

public boolean handle(String command, String [] args) {     
    // lookup and execute the command
    Commands cmd= getCommand(command);
    if(cmd != null) {
        return cmd.doit(0, args);
    }else{
        System.err.println("handle() - Unknown command: " + command);
        return false;
    }       
}

The code for this article can be downloaded from here

Posted in Java  |  Tags java,enum,dispatcher  |  no comments

Comments

(leave url/email »)