This is the second part of the Playing With args4j series. For your convenience you can find other parts in the table of contents in Part 1 – Mixins
Last time we created mixins to be able to compose classes with multiple inherited parameters. We immediately noticed that we have a lot of code duplication because we need to specify parameter names in multiple places. In this part we will see how to cut the boilerplate.
We will implement custom Setter
to automatically populate the fields. Here it is:
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 41 42 43 |
import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.spi.FieldSetter; import org.kohsuke.args4j.spi.Setter; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; public class MixinSetter implements Setter { private ParametersMixin bean; private String name; private Method method; public MixinSetter(ParametersMixin bean, String name, Method method) { this.bean = bean; this.name = name; this.method = method; } @Override public void addValue(Object o) throws CmdLineException { bean.set(name, o); } @Override public Class getType() { return this.method.getReturnType(); } @Override public boolean isMultiValued() { return false; } @Override public FieldSetter asFieldSetter() { return null; } @Override public AnnotatedElement asAnnotatedElement() { return this.method; } } |
In addValue
we just call a bean method populating the dictionary. How do we get the name of the parameter? In this way:
1 2 3 4 5 6 7 8 |
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 MixinSetter((ParametersMixin) bean, o.name(), m), o); } } } |
We just extract the name from the Option
annotation.
Now we can simplify the parameters and remove redundant methods:
1 2 3 4 |
@Option(name="--parameter", usage = "Some parameter") default String parameter(){ return get("Some default value"); } |
We still specify annotation with name and usage. We also need to specify a default value which we do by calling the method get
which is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
default < T> T get(T defaultValue){ for(StackTraceElement e : Thread.currentThread().getStackTrace()){ try { Option o = Class.forName(e.getClassName()).getMethod(e.getMethodName(), new Class[0]).getAnnotation(Option.class); if(o != null){ return get(o.name(), defaultValue); } } catch (NoSuchMethodException | ClassNotFoundException e1) { } } return defaultValue; } |
So we get the stack trace and look for the first method with the Option
annotation (remember about inlining!). When we find it, we just extract the parameter value as usual.
So we still have a strongly typed parameter with getter, and setter is created automatically. So we can do that:
1 |
parameters.parameter() |
And this returns the value of the parameter. However, to set the parameter, we need to do this:
1 |
parameters.set("--parameter", "Value"); |
So we need to have the name again. Can we do better? We will see in the next part.