
org.perfectable.introspection.AnnotationBuilder Maven / Gradle / Ivy
package org.perfectable.introspection;
import org.perfectable.introspection.proxy.InvocationHandler;
import org.perfectable.introspection.proxy.MethodInvocation;
import org.perfectable.introspection.proxy.ProxyBuilder;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Primitives;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.annotations.concurrent.LazyInit;
import static com.google.common.base.Preconditions.checkArgument;
import static org.perfectable.introspection.Introspections.introspect;
/**
* Builder pattern for creating annotation type instances.
*
* Sometimes library methods require passing annotation instance, assuming that client will obtain it from
* reflection on some member or type. This is often poor design, but sometimes some API actually is defined by
* annotations rather than usual program structure.
*
*
In these cases, trying to call those methods, client has two straightforward options:
*
* - Create token members or types with required annotation them and extract this annotation before call.
* - Actually implement annotation interface, either by anonymous class place of call, or as a named somewhere
* near.
*
*
* Both of those solutions are problematic. Beside introduction of unnecessary declaration, they also:
*
* - If different annotation member values are needed, multiple synthetic annotation targets needs to be
* declared.
* - Annotation interfaces as a rule should not be implemented, and static checkers will mark. Implemented
* interface will often have repeated constructs, and should have {@code toString}, {@code hashCode}
* and {@code equals} implemented, but often it doesn't.
*
*
* This class helps create instances of annotation type that are indistinguishable from ones extracted by reflection.
* These have same {@code toString} and {@code hashCode} and {@code equals} that works with native ones.
*
*
If annotation has no members (is effectively a marker), it can be completely constructed by {@link #marker}
* call.
*
*
Otherwise annotation instance should be build starting with {@link #of}, with zero or more calls to {@link #with}
* and then {@link #build}. Every member of the annotation (i.e. annotation interface method) that doesn't have default
* value, needs to have value set, and this requirement is checked in the {@link #build} method.
*
*
Instances of this class are immutable, can be shared between threads and stored in global variables. Products of
* this builder are also immutable, as are all annotation instances.
*
*
Example of marker annotation creation:
*
* Immutable immutableInstance = AnnotationBuilder.marker(Immutable.class);
*
*
* Example of more complex annotation creation:
*
* AnnotationBuilder<Tag> tagBuilder = AnnotationBuilder.of(Tag.class);
* Tag tag1 = tagBuilder.with(Tag::value, "one").build();
* Tag tag2 = tagBuilder.with(Tag::value, "two").build();
* Tags tags = AnnotationBuilder.of(Tags.class).with(Tags::value, new Tag[] { tag1, tag2 }).build();
*
*
* @param annotation type to build
*/
@Immutable
public final class AnnotationBuilder {
/**
* Creates instance of annotation with no members.
*
* If annotation have members, but all of them are default, this method will still
*
*
This method is pure - with the same arguments will produce equivalent results.
*
* @param annotationType representation of type of annotation to build
* @param type of annotation to build
* @return Only annotation instance that can be created for this annotation.
* @throws IllegalArgumentException when provided class does not represent an annotation interface
* @throws IllegalArgumentException when provided annotation type has any members
*/
public static A marker(Class annotationType) {
checkAnnotationInterface(annotationType);
checkArgument(annotationType.getDeclaredMethods().length == 0, "Annotation interface is not a marker");
AnnotationInvocationHandler invocationHandler =
new AnnotationInvocationHandler<>(annotationType, ImmutableMap.of());
return ProxyBuilder.forInterface(annotationType)
.instantiate(invocationHandler);
}
/**
* Creates unconfigured builder for specified annotation type.
*
* @param annotationType representation of type of annotation to build
* @param type of annotation to build
* @return Fresh, unconfigured builder
* @throws IllegalArgumentException when provided class does not represent an annotation interface
*/
public static AnnotationBuilder of(Class annotationType) {
checkAnnotationInterface(annotationType);
return new AnnotationBuilder<>(annotationType, ImmutableMap.of());
}
/**
* Functional reference interface which allows type-safe and refactor-safe extraction of members.
*
* This interface should only be implemented with method reference for a method that is a member of an
* annotation. The method is never actually called in this class.
*
* @param annotation type to mark member on
* @param annotation member type
*/
@FunctionalInterface
public interface MemberExtractor extends FunctionalReference {
@SuppressWarnings({"unused", "javadoc"})
@CanIgnoreReturnValue
X extract(A annotation);
}
/**
* Configures member of created annotation instances to have specified value.
*
* Member is selected by client by providing method reference.
*
*
Builder will reject setting value for a member that has already value configured.
*
* @param member reference to method which represents a configured member
* @param value value for specified member in annotation instance
* @param type of member
* @return new builder, with additional member configured
* @throws IllegalArgumentException when member is not a method reference to member of this builders annotation type
* or when value is not actually instance of member type
*/
public AnnotationBuilder with(MemberExtractor member, X value) {
Method method;
try {
method = member.introspect().referencedMethod();
}
catch (IllegalStateException e) {
throw new IllegalArgumentException(e);
}
checkArgument(method.getDeclaringClass().equals(annotationType),
"Extractor should be a reference to method declared by annotation " + annotationType);
Class> memberType = Primitives.wrap(method.getReturnType());
checkArgument(memberType.isInstance(value),
"Value %s cannot be provided for member %s of type %s", value, method.getName(), memberType);
ImmutableMap newValueMap = ImmutableMap.builder()
.putAll(valueMap).put(method, value).build();
return new AnnotationBuilder<>(annotationType, newValueMap);
}
/**
* Creates annotation instance.
*
* Performs member validation: each non-default member must have configured value.
*
* @return annotation instance with configured members
*/
public A build() {
validateMembers();
AnnotationInvocationHandler invocationHandler =
new AnnotationInvocationHandler<>(annotationType, valueMap);
return ProxyBuilder.forInterface(annotationType)
.instantiate(invocationHandler);
}
private void validateMembers() throws IllegalStateException {
for (Method method : annotationType.getDeclaredMethods()) {
if (method.getDefaultValue() == null && !valueMap.containsKey(method)) {
throw new IllegalStateException("No value set for member '" + method.getName() + "'");
}
}
}
private static void checkAnnotationInterface(Class> annotationType) {
checkArgument(annotationType.isInterface()
&& annotationType.getInterfaces().length == 1
&& annotationType.getInterfaces()[0].equals(Annotation.class),
"Provided class is not an annotation interface");
}
private final Class annotationType;
// values of this map are only immutable types that can be annotation type elements as declared by JLS 9.6.1
@SuppressWarnings("Immutable")
private final ImmutableMap valueMap;
private AnnotationBuilder(Class annotationType, ImmutableMap valueMap) {
this.annotationType = annotationType;
this.valueMap = valueMap;
}
private static final Method ANNOTATION_TYPE_METHOD = introspect(Annotation.class)
.methods()
.named("annotationType")
.unique();
@Immutable
private static final class AnnotationInvocationHandler implements InvocationHandler> {
private static final int UNCALCULATED_HASH_CODE = -1;
private static final int MEMBER_NAME_HASH_MULTIPLIER = 127;
private final Class annotationType;
// values of this map are only immutable types that can be annotation type elements as declared by JLS 9.6.1
@SuppressWarnings("Immutable")
private final ImmutableMap valueMap;
@LazyInit
private volatile int cachedHashCode = UNCALCULATED_HASH_CODE;
@LazyInit
private volatile String cachedRepresentation;
AnnotationInvocationHandler(Class annotationType, ImmutableMap valueMap) {
this.annotationType = annotationType;
this.valueMap = valueMap;
}
@Nullable
@Override
public Object handle(MethodInvocation invocation) {
return invocation.decompose(this::calculateMethodResult);
}
Object calculateMethodResult(Method method, @SuppressWarnings("unused") A receiver, Object... arguments) {
if (ObjectMethods.EQUALS.equals(method)) {
return calculateEquals(arguments[0]);
}
if (ObjectMethods.HASH_CODE.equals(method)) {
return calculateHash();
}
if (ObjectMethods.TO_STRING.equals(method)) {
return calculateRepresentation();
}
if (ANNOTATION_TYPE_METHOD.equals(method)) {
return annotationType;
}
return valueMap.getOrDefault(method, method.getDefaultValue());
}
boolean calculateEquals(Object other) {
if (!(other instanceof Annotation)) {
return false;
}
Annotation otherAnnotation = (Annotation) other;
Class extends Annotation> otherAnnotationType = otherAnnotation.annotationType();
if (!annotationType.equals(otherAnnotationType)) {
return false;
}
for (Method method : annotationType.getDeclaredMethods()) {
Object thisValue = valueMap.getOrDefault(method, method.getDefaultValue());
Object otherValue = safeInvoke(method, other);
if (!Objects.equals(thisValue, otherValue)) {
return false;
}
}
return true;
}
private int calculateHash() {
if (cachedHashCode == UNCALCULATED_HASH_CODE) {
synchronized (this) {
int current = 0;
for (Method method : annotationType.getDeclaredMethods()) {
Object value = valueMap.getOrDefault(method, method.getDefaultValue());
String name = method.getName();
current += MEMBER_NAME_HASH_MULTIPLIER * name.hashCode() ^ Objects.hashCode(value);
}
cachedHashCode = current;
}
}
return cachedHashCode;
}
@SuppressWarnings("ReturnMissingNullable")
private String calculateRepresentation() {
if (cachedRepresentation == null) {
String elements = valueMap.entrySet().stream()
.map(entry -> entry.getKey() + "=" + formatValue(entry.getValue()))
.collect(Collectors.joining(", "));
cachedRepresentation = "@" + annotationType.getName() + '(' + elements + ')';
}
return cachedRepresentation;
}
}
private static String formatValue(Object value) {
if (value instanceof String) {
return String.format("\"%s\"", value);
}
return value.toString();
}
private static Object safeInvoke(Method method, Object target) {
try {
return method.invoke(target);
}
catch (IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
}
}
}