This is the fifth part of the Playing With args4j series. For your convenience you can find other parts in the table of contents in Part 1 – Mixins
There is one more problem with our setters — they are not type safe. This means that we may have integer parameter:
1 2 3 4 |
@Option(name="--parameter", usage = "Some parameter") default int parameter(){ return get(0); } |
And then we can call this:
1 |
parameters.set(parameters::parameter, "1.23"); |
This compiles and crashes in runtime:
1 2 3 4 |
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at IJobParameters.parameter(IJobParameters.java:16) at Program.run(Program.java:35) at Program.main(Program.java:24) |
Can we do something about it? Currently our setter signature is:
1 |
default < T> void set(Supplier<T> getter, T value) |
This means that the line parameters.set(parameters::parameter, "1.23")
is actually treated as parameters.< Object>set(parameters::parameter3, "1.23")
thanks to target typing and covariant return type in method references. See more in http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html.
To fix that we need to stop covariance in some way. We can do this by enforcing the type T
to be invariant by putting it in contravariant position. First trick is:
1 2 3 4 5 6 7 |
default < T> Consumer< T> set(Supplier< T> getter) { return value -> { Object target = getTarget(getter); Method method = MethodReferenceUtils.getReferencedMethod(getter, target); set(method.getAnnotation(Option.class).name(), value); }; } |
Then we set the parameter in this way:
1 |
parameters.set(parameters::parameter).accept(123); |
Because we return the Consumer< T>
where T
is in both covariant and contravariant position, the compiler cannot use Object
as T
and we are safe again. However, this is cumbersome as we need to use accept
method.
There is yet another trick. We need to separate type parameters:
1 2 3 4 5 |
default < T, U extends Supplier< T>> void set(U getter, T value) { Object target = getTarget(getter); Method method = MethodReferenceUtils.getReferencedMethod(getter, target); set(method.getAnnotation(Option.class).name(), value); } |
Thanks to that we can still call:
1 |
parameters.set(parameters::parameter, 123); |
And when we pass String
we get:
1 2 3 |
Error:(34, 23) java: incompatible types: cannot infer type-variable(s) T,U (argument mismatch; bad return type in method reference int cannot be converted to java.lang.String) |
So the final code is:
Base type for mixin:
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 |
import org.kohsuke.args4j.Option; import java.lang.reflect.Method; import java.util.Map; import java.util.function.Supplier; 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); } 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; } default < T, U extends Supplier< T>> void set(U getter, T value) { Method method = MethodReferenceUtils.getReferencedMethod(getter); set(method.getAnnotation(Option.class).name(), value); } } |
Actual interface with parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import org.kohsuke.args4j.Option; public interface IJobParameters extends ParametersMixin { @Option(name="--parameter", usage = "Some parameter") default String parameter(){ return get("Some default value"); } @Option(name="--parameter2", usage = "Some parameter 2") default String parameter2(){ return get("Some default value 2"); } @Option(name="--parameter3", usage = "Some parameter 3") default int parameter3(){ return get(123); } } |
Concrete parameters implementation:
1 2 3 4 5 6 7 8 9 10 11 |
import java.util.HashMap; import java.util.Map; public class ParametersMixinImpl implements ParametersMixin{ private Map< String, Object> parameters = new HashMap<>(); @Override public Map<String, Object> parameters() { return parameters; } } |
1 |
public class JobParameters extends ParametersMixinImpl implements IJobParameters {} |
Helper methods for accessing the lambda instance:
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 44 45 46 47 48 49 50 51 52 53 54 |
import net.bramp.unsafe.UnsafeHelper; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import sun.misc.Unsafe; import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; public class MethodReferenceUtils { @SuppressWarnings("unchecked") public static < T, U> Method getReferencedMethod(Supplier< T> getter) { Object target = MethodReferenceUtils.getTarget(getter); Class< ?> klass = target.getClass(); AtomicReference<Method> ref = new AtomicReference<>(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(klass); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> { ref.set(method); return null; }); try { setTarget(getter, enhancer.create()); getter.get(); } catch (ClassCastException e) { throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", klass)); } Method method = ref.get(); if (method == null) { throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", klass)); } return method; } private static void setTarget(Object lambda, Object target){ long targetAddress = UnsafeHelper.toAddress(target); long lambdaAddress = UnsafeHelper.toAddress(lambda); Unsafe unsafe = UnsafeHelper.getUnsafe(); unsafe.putByte(lambdaAddress + 15, (byte) (targetAddress >> 24 & 0xFF)); unsafe.putByte(lambdaAddress + 14, (byte) (targetAddress >> 16 & 0xFF)); unsafe.putByte(lambdaAddress + 13, (byte) (targetAddress >> 8 & 0xFF)); unsafe.putByte(lambdaAddress + 12, (byte) (targetAddress & 0xFF)); } private static Object getTarget(Object lambda){ byte[] bytes = UnsafeHelper.toByteArray(lambda); int offset = bytes.length - 1; int address = bytes[offset] << 24 | (bytes[offset - 1] & 0xFF) << 16 | (bytes[offset - 2] & 0xFF) << 8 | (bytes[offset - 3] & 0xFF); return UnsafeHelper.fromAddress(address); } } |
Parser for custom setters:
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 |
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 MixinSetter((ParametersMixin) bean, o.name(), m), o); } } } |
Custom setter:
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; } } |
And finally, execution:
1 2 3 4 5 6 7 8 9 10 |
JobParameters parameters = new JobParameters(); CmdLineParser parser = new CmdLineParser(parameters); parseInterfaces(parameters, parser); parser.parseArgument(args); System.out.println("parameter value from command line: " + parameters.parameter()); System.out.println("parameter2 default value: " + parameters.parameter2()); parameters.set(parameters::parameter3, 123); System.out.println("parameter3 value set using setter: " + parameters.parameter3()); |
When you execute this as:
1 |
java Program --parameter "Value from command line" |
You get the following output:
1 2 3 |
parameter value from command line: Value from command line parameter2 default value: Some default value 2 parameter3 value set using setter: 123 |