/* * Copyright 2017-2020 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.micronaut.test.annotation; import static java.util.Arrays.asList; import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import io.micronaut.core.reflect.ReflectionUtils; /** * Common annotation utilities. * @since 1.0.1 */ public final class AnnotationUtils { private AnnotationUtils() { } /** * Find all <em>repeatable</em> {@linkplain Annotation annotations} of * {@code annotationType} that are either <em>present</em>, <em>indirectly * present</em>, or <em>meta-present</em> on the supplied {@link AnnotatedElement}. * * <p>This method extends the functionality of * {@link java.lang.reflect.AnnotatedElement#getAnnotationsByType(Class)} * with additional support for meta-annotations. * * <p>In addition, if the element is a class and the repeatable annotation * is {@link java.lang.annotation.Inherited @Inherited}, this method will * search on superclasses first in order to support top-down semantics. * The result is that this algorithm finds repeatable annotations that * would be <em>shadowed</em> and therefore not visible according to Java's * standard semantics for inherited, repeatable annotations. * * <p>If the element is a class and the repeatable annotation is not * discovered within the class hierarchy, this method will additionally * search on interfaces implemented by each class in the hierarchy. * * <p>If the supplied {@code element} is {@code null}, this method simply * returns an empty list. * * @param element the element to search on, potentially {@code null} * @param annotationType the repeatable annotation type to search for; never {@code null} * @param <A> the annotation instance * @return the list of all such annotations found; neither {@code null} nor mutable * @see java.lang.annotation.Repeatable * @see java.lang.annotation.Inherited */ public static <A extends Annotation> List<A> findRepeatableAnnotations(AnnotatedElement element, Class<A> annotationType) { if (annotationType == null) { throw new IllegalArgumentException("annotationType must not be null"); } Repeatable repeatable = annotationType.getAnnotation(Repeatable.class); if (repeatable == null) { throw new IllegalArgumentException(annotationType.getName() + " must be @Repeatable"); } Class<? extends Annotation> containerType = repeatable.value(); boolean inherited = containerType.isAnnotationPresent(Inherited.class); // Short circuit the search algorithm. if (element == null) { return Collections.emptyList(); } // We use a LinkedHashSet because the search algorithm may discover // duplicates, but we need to maintain the original order. Set<A> found = new LinkedHashSet<>(16); findRepeatableAnnotations(element, annotationType, containerType, inherited, found, new HashSet<>(16)); // unmodifiable since returned from public, non-internal method(s) return Collections.unmodifiableList(new ArrayList<>(found)); } private static <A extends Annotation> void findRepeatableAnnotations(AnnotatedElement element, Class<A> annotationType, Class<? extends Annotation> containerType, boolean inherited, Set<A> found, Set<Annotation> visited) { if (element instanceof Class) { Class<?> clazz = (Class<?>) element; // Recurse first in order to support top-down semantics for inherited, repeatable annotations. if (inherited) { Class<?> superclass = clazz.getSuperclass(); if (superclass != null && superclass != Object.class) { findRepeatableAnnotations(superclass, annotationType, containerType, inherited, found, visited); } } // Search on interfaces for (Class<?> ifc : clazz.getInterfaces()) { if (ifc != Annotation.class) { findRepeatableAnnotations(ifc, annotationType, containerType, inherited, found, visited); } } } // Find annotations that are directly present or meta-present on directly present annotations. findRepeatableAnnotations(element.getDeclaredAnnotations(), annotationType, containerType, inherited, found, visited); // Find annotations that are indirectly present or meta-present on indirectly present annotations. findRepeatableAnnotations(element.getAnnotations(), annotationType, containerType, inherited, found, visited); } @SuppressWarnings("unchecked") private static <A extends Annotation> void findRepeatableAnnotations(Annotation[] candidates, Class<A> annotationType, Class<? extends Annotation> containerType, boolean inherited, Set<A> found, Set<Annotation> visited) { for (Annotation candidate : candidates) { Class<? extends Annotation> candidateAnnotationType = candidate.annotationType(); if (!isInJavaLangAnnotationPackage(candidateAnnotationType) && visited.add(candidate)) { if (candidateAnnotationType.equals(annotationType)) { // Exact match? found.add(annotationType.cast(candidate)); } else if (candidateAnnotationType.equals(containerType)) { // Container? // Note: it's not a legitimate containing annotation type if it doesn't declare // a 'value' attribute that returns an array of the contained annotation type. // See https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.6.3 Method method = ReflectionUtils.getMethod(containerType, "value").orElseThrow( () -> new IllegalStateException(String.format( "Container annotation type '%s' must declare a 'value' attribute of type %s[].", containerType, annotationType))); Annotation[] containedAnnotations = ReflectionUtils.invokeMethod(candidate, method); found.addAll((Collection<? extends A>) asList(containedAnnotations)); } else { // Otherwise search recursively through the meta-annotation hierarchy... findRepeatableAnnotations(candidateAnnotationType, annotationType, containerType, inherited, found, visited); } } } } private static boolean isInJavaLangAnnotationPackage(Class<? extends Annotation> annotationType) { return (annotationType != null && annotationType.getName().startsWith("java.lang.annotation")); } }