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
Show