org.rekex.common_util.AnnoBuilder Maven / Gradle / Ivy
package org.rekex.common_util;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodType;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.function.Function;
/**
* Create an instance of an annotation type `A`, from name values pairs.
*
* Foo foo = AnnoBuilder.of(Foo.class)
* .def("n1", v1)
* .def("n2", v2)
* .build();
*
* Method references can be used instead of names
*
* Foo foo = AnnoBuilder.of(Foo.class)
* .def(Foo::n1, v1)
* .def(Foo::n2, v2)
* .build();
*
* Or use more convenient build methods
*
* Foo foo = AnnoBuilder.build(Foo.class, Foo::n1, v1);
*
* Default values do not need to be supplied.
*/
public class AnnoBuilder
{
/** Return a builder for the annotation type */
public static AnnoBuilder of(Class clazz)
{
return new AnnoBuilder<>(clazz);
}
/** Single-Element Annotation, with the attribute name "value" */
public static A build(Class clazz, Object value)
{
return of(clazz)
.def("value", value)
.build();
}
/** Annotation with one name-value pair */
public static A build(Class clazz,
Function methodRef1, T1 value1)
{
return of(clazz)
.def(methodRef1, value1)
.build();
}
/** Annotation with two name-value pairs */
public static A build(Class clazz,
Function methodRef1, T1 value1,
Function methodRef2, T1 value2)
{
return of(clazz)
.def(methodRef1, value1)
.def(methodRef2, value2)
.build();
}
// for more name-values, chained def() looks better.
final Class clazz;
final HashMap elemMethods = new HashMap<>();
MethodTrap methodTrap;
A trapProxy;
HashMap nameValues = new HashMap<>();
AnnoBuilder(Class clazz)
{
this.clazz = clazz;
for(var method : clazz.getDeclaredMethods())
{
// element methods = declared public instance methods in annotation class, as of Java 16.
if(!Modifier.isPublic(method.getModifiers()))
continue;
if(Modifier.isStatic(method.getModifiers()))
continue;
elemMethods.put(method.getName(), method);
Object defaultValue = method.getDefaultValue();
if(defaultValue!=null)
nameValues.put(method.getName(), defaultValue);
}
methodTrap = new MethodTrap();
@SuppressWarnings("unchecked")
A proxyA = (A)java.lang.reflect.Proxy.newProxyInstance(
clazz.getClassLoader(), new Class>[]{clazz}, methodTrap
);
trapProxy = proxyA;
}
static class MethodTrap implements InvocationHandler
{
Method method;
Object returnValue;
@Override
public Object invoke(Object proxy, Method method, Object[] args)
{
if(this.method!=null)
throw new IllegalArgumentException("multiple methods invoked: "+ List.of(this.method, method));
this.method = method;
// if the method return type is T[], and a single T value is supplied,
// Java allows expression @A(m=value) which is equivalent to @A(m={value}).
// T could be primitive, but never boxed primitive.
this.returnValue = mayWrapInArray(method.getReturnType(), returnValue);
return returnValue; // can't return null if method returns a primitive type.
// if returnValue isn't the correct type for the method, ClassCastException will be thrown
}
}
static Object mayWrapInArray(Class> expectedType, Object value)
{
if(expectedType.isArray() && !expectedType.isInstance(value))
{
Class> expectedComponentType = expectedType.componentType();
// if value is boxed primitive type, get the primitive type
Class> valueType = unboxType(value.getClass());
if(expectedComponentType==valueType)
{
var array = Array.newInstance(valueType, 1);
Array.set(array, 0, value); // it handles boxed primitive value
return array;
}
}
return value;
}
static Class> unboxType(Class> clazz) // e.g. Integer -> int
{
return MethodType.methodType(clazz).unwrap().returnType();
}
// T doesn't actually provides static type safety, because it could be inferred as Object.
// It also matches def(A->X[], X), which we'll silently convert value to {value}.
// If value isn't compatible with the return type of methodRef, an exception will be thrown.
/** Define a name-value pair. */
public AnnoBuilder def(Function methodRef, T value)
{
if(value==null)
throw new NullPointerException("annotation element value cannot be null");
// invoke methodRef on trapProxy to figure out which method was invoked
methodTrap.method = null;
methodTrap.returnValue = value;
methodRef.apply(trapProxy); // throws ClassCastException; see MethodTrap.invoke()
var method = methodTrap.method;
if(method==null || !elemMethods.containsKey(method.getName()))
throw new IllegalArgumentException("methodRef did not invoke one of the element methods in "+clazz);
// it's allowed to redefine an element with a new value.
// this is necessary to override default values.
// also useful for reusing a builder for multiple annos.
String name = method.getName();
nameValues.put(name, methodTrap.returnValue);
return this;
}
/** Define a name-value pair. */
public AnnoBuilder def(String name, Object value)
{
Method method = elemMethods.get(name);
if(method==null)
throw new IllegalArgumentException("not an element name: "+name);
// check that value is the correct type for the method
methodTrap.method = null;
methodTrap.returnValue = value;
try
{
method.setAccessible(true);
method.invoke(trapProxy); // throws ClassCastException; see MethodTrap.invoke()
}
catch (Exception e)
{
if(e instanceof InvocationTargetException e1)
if(e1.getTargetException() instanceof ClassCastException e2)
throw e2;
throw new RuntimeException(e);
}
nameValues.put(name, methodTrap.returnValue);
return this;
}
/** Return an instance with previously defined name-value pairs. */
public A build()
{
if(nameValues.size()(elemMethods.keySet());
diff.removeAll(nameValues.keySet());
throw new IllegalStateException("value undefined for "+diff);
}
var elemValuesClone = new HashMap<>(nameValues);
var handler = new AnnoInvoHandler<>(clazz, elemValuesClone);
@SuppressWarnings("unchecked")
A proxyA = (A)java.lang.reflect.Proxy.newProxyInstance(
clazz.getClassLoader(), new Class>[]{clazz}, handler
);
return proxyA;
}
// `a0` implements an annotation interface;
// but it may not have implemented hashCode/equals/toString() correctly.
// (even jdk is buggy: https://bugs.openjdk.java.net/browse/JDK-8268788 )
// this method will return an object that conforms to the spec.
// And our toString() method returns a representation that can be compiled
// by javac back to the same anno; JDK's does not; may not even compile.
/**
* Return an instance with same name-value paris as `a0`,
* and also with correct implementations of hashCode/equals/toString
* methods according to {@link Annotation}
*/
public static A build(A a0)
{
@SuppressWarnings("unchecked")
Class clazz = (Class)a0.annotationType();
AnnoBuilder builder = new AnnoBuilder<>(clazz);
builder.elemMethods.forEach((name, method)->{
try
{
method.setAccessible(true);
Object value = method.invoke(a0);
builder.nameValues.put(name, value);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
});
return builder.build();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy