This is the first part of the Playing With args4j series. For your convenience you can find other parts using the links below (or by guessing the address):
Part 1 — Mixins
Part 2 — Automatic getters
Part 3 — Nice setters
Part 4 — Nicer setters
Part 5 — Safe setters
Imagine that you are working on a command line tool and you need to parse arguments somehow. You can use pretty nice library called args4j.
So you write a class for your job parameters:
1 2 3 4 5 6 7 8 |
import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; public class JobParameters { @Option(name="--parameter", usage = "Some parameter") private String parameter = "value1"; } |
And then you parse it:
1 2 3 4 5 6 7 |
public class Job { public void run(){ JobParameters parameters = new JobParameters(); CmdLineParser parser = new CmdLineParser(parameters); parser.parseArgument(args); } } |
Looks great. Some time later you write another job with another parameter:
1 2 3 4 |
public class JobParameters2 { @Option(name="--parameter2", usage = "Some parameter2") private String parameter2 = "valu2"; } |
Time goes by and you need to write another job which should have both of the parameters. What can you do? This is Java so you can’t use multiple inheritance of state. However, there are default interface implementations which we can use to simulate mixins!
Let’s start with this:
1 2 3 4 5 6 7 8 9 10 11 |
public interface ParametersMixin { Map<String, Object> parameters(); default < T> T get(String name, T defaultValue){ return (T)parameters().getOrDefault(name, defaultValue); } default < T> void set(String name, T value){ parameters().put(name, value); } } |
We create an interface with methods for getting and setting parameters. We also have one abstract method providing collection. We could use field here but then it would be static so we wouldn’t be able to create multiple instances at the same time.
Now, let’s rework first parameter class:
1 2 3 4 5 6 7 8 9 10 |
public interface IJobParameters extends ParametersMixin { default String parameter(){ return get("parameter", "value1"); } @Option(name="--parameter", usage = "Some parameter") default void parameter(String parameter){ set("parameter", parameter); } } |
The same goes for second parameter class:
1 2 3 4 5 6 7 8 9 10 |
public interface IJobParameters2 extends ParametersMixin { default String parameter2(){ return get("parameter2", "value2"); } @Option(name="--parameter2", usage = "Some parameter2") default void parameter2(String parameter2){ set("parameter2", parameter2); } } |
Now we need to have an implementation for collection:
1 2 3 4 5 6 7 |
class ParametersMixinImpl implements ParametersMixin{ private Map<String, Object> parameters = new HashMap<>(); @Override public Map<String, Object> parameters() { return parameters; } } |
One more thing: we need to have concrete classes with parameters but this is trivial:
1 2 |
public class JobParameters extends ParametersMixinImpl implements IJobParameters {} public class JobParameters2 extends ParametersMixinImpl implements IJobParameters2 {} |
Awesome… but it doesn’t work. The reason is that args4j doesn’t support methods on interfaces — it just doesn’t parse them. Let’s fix that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public void run(){ JobParameters parameters = new JobParameters(); CmdLineParser parser = new CmdLineParser(parameters); parseInterfaces(parameters, parser); parser.parseArgument(args); } private void parseInterfaces(Object bean, CmdLineParser parser){ // args4j doesn't support methods on interfaces, we need to parse them manually Set<Class> interfaces = new HashSet<>(); Deque<Class> classes = new ArrayDeque<>(); classes.add(bean.getClass()); while(!classes.isEmpty()){ Class c = classes.getFirst(); classes.removeFirst(); if(c.isInterface()){ interfaces.add(c); } if(c.getSuperclass() != null){ classes.addLast(c.getSuperclass()); } classes.addAll(Arrays.asList(c.getInterfaces())); } for(Class c : interfaces) { parseInterfaceMethods(c, bean, parser); } } private void parseInterfaceMethods(Class c, Object bean, CmdLineParser parser){ for(Method m : c.getDeclaredMethods()){ Option o = m.getAnnotation(Option.class); if(o != null){ parser.addOption(new MethodSetter(parser, bean, m), o); } } } |
Yep, that was long but now we are ready to create class for third job:
1 |
public class JobParameters3 extends ParametersMixinImpl implements IJobParameters, IJobParameters2 {} |
Done! I believe you can clearly see the advantages and that doing that in such a simple example is not very beneficial but can be great if you have multiple combinations of parameters for different jobs.
There is one terrible thing, though. Instead of having one field we need to have two methods now. We will fix that one day.