Java annotations make declarative programming easy
Update: I've started
adding some more the features that I talked about and some suggestions of people
who were interested in the project. I added property access (just put the
annotation on the set method), type conversions (through String constructors),
and a usage message. You can checkout the
code from here.Parsing
command line arguments usually involves using some big library like common-cli
from apache or doing some quick hack. This sort of like a combination of the
two in so much as it is quick hack but its really easy to use and powerful. I'm
going to forgoe the usage message for now to keep it really simple, though
extending it with usage and all the other cute CLI extensions should be quite
straight-forward. The first thing we will need is a nice little
annotation:
package com.sampullara.cli;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Argument {
String value() default "";
boolean required() default false;
String description() default "";
}
Now let's look at an example of what we want
the usage of the annotation to look like. Say we are going to make something
takes an input and and output file. The simplest case would probably look
something like:
package example;
import com.sampullara.cli.Argument;
import com.sampullara.cli.Args;
import java.util.List;
public class InputOutput {
@Argument(value = "input", description = "This is the input file", required = true)
private String inputFilename;
@Argument(value = "output", description = "This is the output file", required = true)
private String outputFilename;
@Argument(description = "This flag can optionally be set")
private boolean someflag;
public static void main(String[] args) {
InputOutput io = new InputOutput();
List<String> extra = Args.parse(io, args);
io.doit(extra);
}
public void doit(List<String> extra) {
System.out.println("Input: " + inputFilename);
System.out.println("Output: " + outputFilename);
System.out.println("Someflag: " + someflag);
System.out.println("Extra: " + extra);
}
}
Thats certainly quite a bit easier than
writing an XML document and I can see right in the code what the arguments are
going to look like without thinking too much about it. Now I just have to
implement that pesky Args class so we can actually get the values of the
arguments. This code could be better:
/*
* Copyright (c) 2005, Sam Pullara. All Rights Reserved.
* You may modify and redistribute as long as this attribution remains.
*/
package com.sampullara.cli;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class Args {
public static List parse(Object target, String[] args) {
List arguments = new ArrayList();
arguments.addAll(Arrays.asList(args));
Class clazz = target.getClass();
for (Field field : clazz.getDeclaredFields()) {
Argument argument = field.getAnnotation(Argument.class);
if (argument != null) {
boolean set = false;
for (Iterator i = arguments.iterator(); i.hasNext();) {
String arg = i.next();
if (arg.startsWith("-")) {
Object value;
String name = argument.value();
if (name.equals("")) {
name = field.getName();
}
if (arg.substring(1).equals(name)) {
i.remove();
Class type = field.getType();
if (type == Boolean.TYPE || type == Boolean.class) {
value = true;
} else {
if (i.hasNext()) {
value = i.next();
i.remove();
} else {
throw new IllegalArgumentException("Must have a value for non-boolean argument " + argument.value());
}
}
setField(field, target, value);
set = true;
}
if (set) break;
}
}
if (!set && argument.required()) {
throw new IllegalArgumentException("You must set argument " + argument.value());
}
}
}
for (String argument : arguments) {
if (argument.startsWith("-")) {
throw new IllegalArgumentException("Invalid argument: " + argument);
}
}
return arguments;
}
private static void setField(Field field, Object target, Object value) {
if (!Modifier.isPublic(field.getModifiers())) {
field.setAccessible(true);
}
try {
field.set(target, value);
} catch (IllegalAccessException iae) {
throw new IllegalArgumentException("Could not set field " + field, iae);
}
}
}
One of the other cool things about this is
that I don't need to specify the default for an argument in the annotation,
instead I can just set the variable to the default which is much more intuitive
and DRY. You could probably spend an afternoon on this code and make a really
nice system with detailed usage messages, advanced options like allowing Arrays
with delimited values, doing conversions for more than just booleans (File, int,
etc), calling setters rather than setting fields for validation, and possibly
even just integrating a set of annotations into common-cli rather than
reinventing the wheel. Or you could just use this tiny amount of code and not
think about it much.Anyway, the point
is that annotations can make declarative programming very simple, you just need
a little bit of reflection magic to get
started.Complete project with build
file:cli.jar
Posted: Sat
- December
24, 2005 at 10:59 AM
|