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

org.perfectable.introspection.query.AnnotationQuery Maven / Gradle / Ivy

There is a newer version: 5.1.0
Show newest version
package org.perfectable.introspection.query;

import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import static java.util.Objects.requireNonNull;

/**
 * Iterable-like container that finds annotations.
 *
 * 

Instances of this class are immutable, each filtering produces new, modified instance. To obtain query for * specific class, use {@link #of}. * *

To obtain results either iterate this class with {@link #iterator} (or in enhanced-for loop) or use one of * {@link #stream()}, {@link #unique()}, {@link #option()} or {@link #isPresent()}. * *

This query handles repeatable annotations on elements as if they would be placed in a container annotation, * exactly as it is written in bytecode. To get behavior that was introduced in Java 1.8 in * {@link AnnotatedElement#getDeclaredAnnotationsByType}, this query needs manual expansion of container annotations. * See {@link #withRepeatableUnroll} for details. * *

Example usage, which collects all annotations on {@code singletonClass} that are meta-annotated by Qualifier. *

 *     AnnotationQuery.of(singletonClass)
 * 			.annotatedWith(javax.inject.Qualifier.class)
 * 			.stream()
 * 			.collect(ImmutableSet.toImmutableSet());
 * 
* * @param base annotation type */ @SuppressWarnings({ "DesignForExtension" // class is closed because of package-private constructor }) public abstract class AnnotationQuery extends AbstractQuery> { /** * This simple Null Object Pattern for this query. * * @return Query which will produce no results. */ public static AnnotationQuery empty() { return Empty.INSTANCE; } /** * Creates a query from annotations declared on specified element. * *

Only annotations with {@link java.lang.annotation.RetentionPolicy#RUNTIME} will be returned. * * @param element element to extract annotations from * @return annotation query that will list annotations */ public static AnnotationQuery of(AnnotatedElement element) { return new OfElement(element); } /** * Converts set of annotations to query. * *

Sets stream can be used to filter and does essentially the same, but this query allows more expressive filters * used on specified set. * * @param set set to search on * @return annotation query that will list exactly the set contents */ public static AnnotationQuery fromElements(Set set) { return new OfSet(ImmutableSet.copyOf(set)); } /** * Converts array of annotations to query. * *

Stream of array contents can be used to filter and does essentially the same, but this query allows more * expressive filters used. * * @param elements annotations to search from to search on * @return annotation query that will list exactly the array contents */ public static AnnotationQuery fromElements(Annotation... elements) { return fromElements(ImmutableSet.copyOf(elements)); } /** * Creates query that contains results from both this and provided query. * * @param other annotation query to add * @return annotation query that will list results of this and then the other query */ @SuppressWarnings("unchecked") public AnnotationQuery join(AnnotationQuery other) { return Composite.composite(this, other); } @Override public AnnotationQuery filter(Predicate filter) { requireNonNull(filter); return new Predicated<>(this, filter); } @Override public AnnotationQuery sorted(Comparator comparator) { requireNonNull(comparator); return new Sorted<>(this, comparator); } /** * Creates query that filters resulting annotations that have meta-annotation placed on them. * * @param metaAnnotation annotation that must be placed on resulting annotation to be returned by query * @return annotation query that will list results that have specific annotations placed on them */ public AnnotationQuery annotatedWith(Class metaAnnotation) { requireNonNull(metaAnnotation); return new Annotated<>(this, metaAnnotation); } /**. * Creates query that filters annotation that have specific type as a supertype. * * @param annotationClass annotation type to filters * @param new query result type * @return annotation query that will list results that are of specific type */ public AnnotationQuery typed(Class annotationClass) { requireNonNull(annotationClass); return new Typed<>(this, annotationClass); } /** * Expands repeatable annotations for the results of returned query. * *

Normally, query handles repeatable annotations on elements as if they would be placed in a container * annotation, exactly as it is written in bytecode. This method will add that repeatable annotations to the * container. * *

This method changes results, so that every annotation, that would be returned, if its a * containing annotation, annotations that are embedded in it are also returned as a results. Containing * annotation is also returned, if it matches other filters. See JLS 9.6.3. * *

Unfortunately, repeatable annotation extraction in native Java is done with special care that is unavailable * for public, for example in OpenJDK, the sun.reflect.annotation.AnnotationType is used, which is contained in a * Class instance of an annotation type. Both field containing and the type is not public. Therefore this * query tries to reconstruct relationship between containing and contained types. This has some initial performance * penalty, as "value" method must be reflected. This class caches the information, so successive calls should * be faster. * * * @return annotation query that will list repeatable annotations extracted from containers */ public AnnotationQuery withRepeatableUnroll() { return new RepeatableUnroll(this); } private static final class OfElement extends AnnotationQuery { private final AnnotatedElement element; OfElement(AnnotatedElement element) { this.element = element; } @Override public Stream stream() { return Stream.of(element.getAnnotations()); } } private static final class OfSet extends AnnotationQuery { private final ImmutableSet elements; OfSet(ImmutableSet elements) { this.elements = elements; } @Override public Stream stream() { return elements.stream(); } } private abstract static class Filtered extends AnnotationQuery { private final AnnotationQuery parent; Filtered(AnnotationQuery parent) { this.parent = parent; } protected abstract boolean matches(A candidate); @Override public Stream stream() { return this.parent.stream() .filter(this::matches); } @SuppressWarnings("unchecked") @Override public boolean contains(@Nullable Object candidate) { if (!(candidate instanceof Annotation)) { return false; } return matches((A) candidate) && parent.contains(candidate); } } private static final class Predicated extends Filtered { private final Predicate filter; Predicated(AnnotationQuery parent, Predicate filter) { super(parent); this.filter = filter; } @Override protected boolean matches(A candidate) { return filter.test(candidate); } } private static final class Sorted extends AnnotationQuery { private final AnnotationQuery parent; private final Comparator comparator; Sorted(AnnotationQuery parent, Comparator comparator) { this.parent = parent; this.comparator = comparator; } @Override public AnnotationQuery sorted(Comparator nextComparator) { @SuppressWarnings("unchecked") Comparator<@Nullable Object> casted = (Comparator<@Nullable Object>) nextComparator; return new Sorted<>(parent, this.comparator.thenComparing(casted)); } @Override public Stream stream() { return parent.stream().sorted(comparator); } @Override public boolean contains(@Nullable Object candidate) { return parent.contains(candidate); } } private static final class Annotated extends Filtered { private final Class metaAnnotation; Annotated(AnnotationQuery parent, Class metaAnnotation) { super(parent); this.metaAnnotation = metaAnnotation; } @Override protected boolean matches(A candidate) { return candidate.annotationType().isAnnotationPresent(metaAnnotation); } } private static final class Typed extends AnnotationQuery { private final AnnotationQuery parent; private final Class type; Typed(AnnotationQuery parent, Class type) { this.parent = parent; this.type = type; } @Override public Stream stream() { return parent.stream() .filter(type::isInstance) .map(type::cast); } @Override public boolean contains(@Nullable Object candidate) { return type.isInstance(candidate) && parent.contains(candidate); } } private static final class Empty extends AnnotationQuery { private static final Empty INSTANCE = new Empty(); @Override public Stream stream() { return Stream.empty(); } @Override public Empty filter(Predicate filter) { return Empty.INSTANCE; } @SuppressWarnings("unchecked") @Override public AnnotationQuery join(AnnotationQuery other) { return (AnnotationQuery) other; } @Override public boolean contains(@Nullable Object candidate) { return false; } } private static final class RepeatableUnroll extends AnnotationQuery { private static final Set> KNOWN_NON_CONTAINERS = Collections.newSetFromMap(new ConcurrentHashMap<>()); private static final Map, Method> KNOWN_CONTAINERS = new ConcurrentHashMap<>(); private final AnnotationQuery parent; RepeatableUnroll(AnnotationQuery parent) { this.parent = parent; } @Override public Stream stream() { return parent.stream() .flatMap(this::unroll); } @Override public boolean contains(@Nullable Object candidate) { if (parent.contains(candidate)) { return true; } if (!(candidate instanceof Annotation)) { return false; } Annotation candidateAnnotation = (Annotation) candidate; @Nullable Repeatable repeatableAnnotation = candidateAnnotation.annotationType().getAnnotation(Repeatable.class); if (repeatableAnnotation == null) { return false; } Class containerType = repeatableAnnotation.value(); Method extractionMethod = KNOWN_CONTAINERS.computeIfAbsent(containerType, RepeatableUnroll::findContainerMethod); AnnotationQuery typed = parent.typed(containerType); return typed.stream() .flatMap(container -> extractContents(container, extractionMethod)) .anyMatch(candidate::equals); } private Stream unroll(Annotation source) { Stream baseResult = Stream.of(source); Class sourceClass = source.annotationType(); if (KNOWN_NON_CONTAINERS.contains(sourceClass)) { return baseResult; } try { Method extractionMethod = KNOWN_CONTAINERS.computeIfAbsent(sourceClass, RepeatableUnroll::findContainerMethod); Stream additionalResults = extractContents(source, extractionMethod); return Stream.concat(baseResult, additionalResults); } catch (IllegalArgumentException e) { KNOWN_NON_CONTAINERS.add(sourceClass); return baseResult; } } private static Method findContainerMethod(Class candidateContainer) throws IllegalArgumentException { Method[] methods = candidateContainer.getDeclaredMethods(); Optional valueMethodOption = Stream.of(methods).filter(method -> method.getName().equals("value")).findAny(); if (!valueMethodOption.isPresent()) { throw new IllegalArgumentException(); } Method valueMethod = valueMethodOption.get(); Class returnType = valueMethod.getReturnType(); if (!returnType.isArray() || !Annotation.class.isAssignableFrom(returnType.getComponentType())) { throw new IllegalArgumentException(); } @SuppressWarnings("unchecked") Class containedClass = (Class) returnType.getComponentType(); @Nullable Repeatable repeatableAnnotation = containedClass.getAnnotation(Repeatable.class); if (repeatableAnnotation == null || !repeatableAnnotation.value().equals(candidateContainer)) { throw new IllegalArgumentException(); } return valueMethod; } @SuppressWarnings("cast.unsafe") public Stream extractContents(Annotation source, Method extractionMethod) { Object resultArray; try { resultArray = (@NonNull Object) extractionMethod.invoke(source); } catch (IllegalAccessException | InvocationTargetException e) { throw new LinkageError(e.getMessage(), e); } return IntStream.range(0, Array.getLength(resultArray)) .mapToObj(i -> (Annotation) Array.get(resultArray, i)); } } private static final class Composite extends AnnotationQuery { private final ImmutableList> components; private Composite(ImmutableList> components) { this.components = components; } @SuppressWarnings("unchecked") private static AnnotationQuery composite(AnnotationQuery... components) { return new Composite<>(ImmutableList.copyOf(components)); } @Override public Stream stream() { return components.stream().flatMap(AbstractQuery::stream); } @Override public AnnotationQuery join(AnnotationQuery other) { ImmutableList> newComponents = ImmutableList.>builder() .addAll(components).add(other).build(); return new Composite(newComponents); } @Override public boolean contains(@Nullable Object candidate) { return components.stream().anyMatch(component -> component.contains(candidate)); } } AnnotationQuery() { // package extension only } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy