All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy