org.springframework.test.util.MetaAnnotationUtils Maven / Gradle / Ivy
Show all versions of spring-test Show documentation
/*
* Copyright 2002-2018 the original author or 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
*
* http://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 org.springframework.test.util;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.Set;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* {@code MetaAnnotationUtils} is a collection of utility methods that complements
* the standard support already available in {@link AnnotationUtils}.
*
* Whereas {@code AnnotationUtils} provides utilities for getting or
* finding an annotation, {@code MetaAnnotationUtils} goes a step further
* by providing support for determining the root class on which an
* annotation is declared, either directly or indirectly via a composed
* annotation. This additional information is encapsulated in an
* {@link AnnotationDescriptor}.
*
*
The additional information provided by an {@code AnnotationDescriptor} is
* required by the Spring TestContext Framework in order to be able to
* support class hierarchy traversals for annotations such as
* {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration},
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners},
* and {@link org.springframework.test.context.ActiveProfiles @ActiveProfiles}
* which offer support for merging and overriding various inherited
* annotation attributes (e.g.
* {@link org.springframework.test.context.ContextConfiguration#inheritLocations}).
*
* @author Sam Brannen
* @since 4.0
* @see AnnotationUtils
* @see AnnotationDescriptor
*/
public abstract class MetaAnnotationUtils {
/**
* Find the {@link AnnotationDescriptor} for the supplied {@code annotationType}
* on the supplied {@link Class}, traversing its annotations, interfaces, and
* superclasses if no annotation can be found on the given class itself.
*
This method explicitly handles class-level annotations which are not
* declared as {@linkplain java.lang.annotation.Inherited inherited} as
* well as meta-annotations.
*
The algorithm operates as follows:
*
* - Search for the annotation on the given class and return a corresponding
* {@code AnnotationDescriptor} if found.
*
- Recursively search through all annotations that the given class declares.
*
- Recursively search through all interfaces implemented by the given class.
*
- Recursively search through the superclass hierarchy of the given class.
*
* In this context, the term recursively means that the search
* process continues by returning to step #1 with the current annotation,
* interface, or superclass as the class to look for annotations on.
* @param clazz the class to look for annotations on
* @param annotationType the type of annotation to look for
* @return the corresponding annotation descriptor if the annotation was found;
* otherwise {@code null}
* @see AnnotationUtils#findAnnotationDeclaringClass(Class, Class)
* @see #findAnnotationDescriptorForTypes(Class, Class...)
*/
@Nullable
public static AnnotationDescriptor findAnnotationDescriptor(
Class> clazz, Class annotationType) {
return findAnnotationDescriptor(clazz, new HashSet<>(), annotationType);
}
/**
* Perform the search algorithm for {@link #findAnnotationDescriptor(Class, Class)},
* avoiding endless recursion by tracking which annotations have already been
* visited.
* @param clazz the class to look for annotations on
* @param visited the set of annotations that have already been visited
* @param annotationType the type of annotation to look for
* @return the corresponding annotation descriptor if the annotation was found;
* otherwise {@code null}
*/
@Nullable
private static AnnotationDescriptor findAnnotationDescriptor(
@Nullable Class> clazz, Set visited, Class annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
if (clazz == null || Object.class == clazz) {
return null;
}
// Declared locally?
if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) {
return new AnnotationDescriptor<>(clazz, clazz.getAnnotation(annotationType));
}
// Declared on a composed annotation (i.e., as a meta-annotation)?
for (Annotation composedAnn : clazz.getDeclaredAnnotations()) {
Class extends Annotation> composedType = composedAnn.annotationType();
if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedType.getName()) && visited.add(composedAnn)) {
AnnotationDescriptor descriptor = findAnnotationDescriptor(composedType, visited, annotationType);
if (descriptor != null) {
return new AnnotationDescriptor<>(
clazz, descriptor.getDeclaringClass(), composedAnn, descriptor.getAnnotation());
}
}
}
// Declared on interface?
for (Class> ifc : clazz.getInterfaces()) {
AnnotationDescriptor descriptor = findAnnotationDescriptor(ifc, visited, annotationType);
if (descriptor != null) {
return new AnnotationDescriptor<>(clazz, descriptor.getDeclaringClass(),
descriptor.getComposedAnnotation(), descriptor.getAnnotation());
}
}
// Declared on a superclass?
return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType);
}
/**
* Find the {@link UntypedAnnotationDescriptor} for the first {@link Class}
* in the inheritance hierarchy of the specified {@code clazz} (including
* the specified {@code clazz} itself) which declares at least one of the
* specified {@code annotationTypes}.
* This method traverses the annotations, interfaces, and superclasses
* of the specified {@code clazz} if no annotation can be found on the given
* class itself.
*
This method explicitly handles class-level annotations which are not
* declared as {@linkplain java.lang.annotation.Inherited inherited} as
* well as meta-annotations.
*
The algorithm operates as follows:
*
* - Search for a local declaration of one of the annotation types on
* the given class and return a corresponding {@code UntypedAnnotationDescriptor}
* if found.
*
- Recursively search through all annotations that the given class declares.
*
- Recursively search through all interfaces implemented by the given class.
*
- Recursively search through the superclass hierarchy of the given class.
*
* In this context, the term recursively means that the search
* process continues by returning to step #1 with the current annotation,
* interface, or superclass as the class to look for annotations on.
* @param clazz the class to look for annotations on
* @param annotationTypes the types of annotations to look for
* @return the corresponding annotation descriptor if one of the annotations
* was found; otherwise {@code null}
* @see AnnotationUtils#findAnnotationDeclaringClassForTypes(java.util.List, Class)
* @see #findAnnotationDescriptor(Class, Class)
*/
@SuppressWarnings("unchecked")
@Nullable
public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(
Class> clazz, Class extends Annotation>... annotationTypes) {
return findAnnotationDescriptorForTypes(clazz, new HashSet<>(), annotationTypes);
}
/**
* Perform the search algorithm for {@link #findAnnotationDescriptorForTypes(Class, Class...)},
* avoiding endless recursion by tracking which annotations have already been
* visited.
* @param clazz the class to look for annotations on
* @param visited the set of annotations that have already been visited
* @param annotationTypes the types of annotations to look for
* @return the corresponding annotation descriptor if one of the annotations
* was found; otherwise {@code null}
*/
@SuppressWarnings("unchecked")
@Nullable
private static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(@Nullable Class> clazz,
Set visited, Class extends Annotation>... annotationTypes) {
assertNonEmptyAnnotationTypeArray(annotationTypes, "The list of annotation types must not be empty");
if (clazz == null || Object.class == clazz) {
return null;
}
// Declared locally?
for (Class extends Annotation> annotationType : annotationTypes) {
if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) {
return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType));
}
}
// Declared on a composed annotation (i.e., as a meta-annotation)?
for (Annotation composedAnnotation : clazz.getDeclaredAnnotations()) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) {
UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(
composedAnnotation.annotationType(), visited, annotationTypes);
if (descriptor != null) {
return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(),
composedAnnotation, descriptor.getAnnotation());
}
}
}
// Declared on interface?
for (Class> ifc : clazz.getInterfaces()) {
UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(ifc, visited, annotationTypes);
if (descriptor != null) {
return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(),
descriptor.getComposedAnnotation(), descriptor.getAnnotation());
}
}
// Declared on a superclass?
return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes);
}
private static void assertNonEmptyAnnotationTypeArray(Class>[] annotationTypes, String message) {
if (ObjectUtils.isEmpty(annotationTypes)) {
throw new IllegalArgumentException(message);
}
for (Class> clazz : annotationTypes) {
if (!Annotation.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Array elements must be of type Annotation");
}
}
}
/**
* Descriptor for an {@link Annotation}, including the {@linkplain
* #getDeclaringClass() class} on which the annotation is declared
* as well as the actual {@linkplain #getAnnotation() annotation} instance.
* If the annotation is used as a meta-annotation, the descriptor also includes
* the {@linkplain #getComposedAnnotation() composed annotation} on which the
* annotation is present. In such cases, the root declaring class is
* not directly annotated with the annotation but rather indirectly via the
* composed annotation.
*
Given the following example, if we are searching for the {@code @Transactional}
* annotation on the {@code TransactionalTests} class, then the
* properties of the {@code AnnotationDescriptor} would be as follows.
*
* - rootDeclaringClass: {@code TransactionalTests} class object
* - declaringClass: {@code TransactionalTests} class object
* - composedAnnotation: {@code null}
* - annotation: instance of the {@code Transactional} annotation
*
*
* @Transactional
* @ContextConfiguration({"/test-datasource.xml", "/repository-config.xml"})
* public class TransactionalTests { }
*
* Given the following example, if we are searching for the {@code @Transactional}
* annotation on the {@code UserRepositoryTests} class, then the
* properties of the {@code AnnotationDescriptor} would be as follows.
*
* - rootDeclaringClass: {@code UserRepositoryTests} class object
* - declaringClass: {@code RepositoryTests} class object
* - composedAnnotation: instance of the {@code RepositoryTests} annotation
* - annotation: instance of the {@code Transactional} annotation
*
*
* @Transactional
* @ContextConfiguration({"/test-datasource.xml", "/repository-config.xml"})
* @Retention(RetentionPolicy.RUNTIME)
* public @interface RepositoryTests { }
*
* @RepositoryTests
* public class UserRepositoryTests { }
*
*
* @param the annotation type
*/
public static class AnnotationDescriptor {
private final Class> rootDeclaringClass;
private final Class> declaringClass;
@Nullable
private final Annotation composedAnnotation;
private final T annotation;
private final AnnotationAttributes annotationAttributes;
public AnnotationDescriptor(Class> rootDeclaringClass, T annotation) {
this(rootDeclaringClass, rootDeclaringClass, null, annotation);
}
public AnnotationDescriptor(Class> rootDeclaringClass, Class> declaringClass,
@Nullable Annotation composedAnnotation, T annotation) {
Assert.notNull(rootDeclaringClass, "'rootDeclaringClass' must not be null");
Assert.notNull(annotation, "Annotation must not be null");
this.rootDeclaringClass = rootDeclaringClass;
this.declaringClass = declaringClass;
this.composedAnnotation = composedAnnotation;
this.annotation = annotation;
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
rootDeclaringClass, annotation.annotationType().getName(), false, false);
Assert.state(attributes != null, "No annotation attributes");
this.annotationAttributes = attributes;
}
public Class> getRootDeclaringClass() {
return this.rootDeclaringClass;
}
public Class> getDeclaringClass() {
return this.declaringClass;
}
public T getAnnotation() {
return this.annotation;
}
/**
* Synthesize the merged {@link #getAnnotationAttributes AnnotationAttributes}
* in this descriptor back into an annotation of the target
* {@linkplain #getAnnotationType annotation type}.
* @since 4.2
* @see #getAnnotationAttributes()
* @see #getAnnotationType()
* @see AnnotationUtils#synthesizeAnnotation(java.util.Map, Class, java.lang.reflect.AnnotatedElement)
*/
@SuppressWarnings("unchecked")
public T synthesizeAnnotation() {
return AnnotationUtils.synthesizeAnnotation(
getAnnotationAttributes(), (Class) getAnnotationType(), getRootDeclaringClass());
}
public Class extends Annotation> getAnnotationType() {
return this.annotation.annotationType();
}
public AnnotationAttributes getAnnotationAttributes() {
return this.annotationAttributes;
}
@Nullable
public Annotation getComposedAnnotation() {
return this.composedAnnotation;
}
@Nullable
public Class extends Annotation> getComposedAnnotationType() {
return (this.composedAnnotation != null ? this.composedAnnotation.annotationType() : null);
}
/**
* Provide a textual representation of this {@code AnnotationDescriptor}.
*/
@Override
public String toString() {
return new ToStringCreator(this)
.append("rootDeclaringClass", this.rootDeclaringClass)
.append("declaringClass", this.declaringClass)
.append("composedAnnotation", this.composedAnnotation)
.append("annotation", this.annotation)
.toString();
}
}
/**
* Untyped extension of {@code AnnotationDescriptor} that is used
* to describe the declaration of one of several candidate annotation types
* where the actual annotation type cannot be predetermined.
*/
public static class UntypedAnnotationDescriptor extends AnnotationDescriptor {
public UntypedAnnotationDescriptor(Class> rootDeclaringClass, Annotation annotation) {
this(rootDeclaringClass, rootDeclaringClass, null, annotation);
}
public UntypedAnnotationDescriptor(Class> rootDeclaringClass, Class> declaringClass,
@Nullable Annotation composedAnnotation, Annotation annotation) {
super(rootDeclaringClass, declaringClass, composedAnnotation, annotation);
}
/**
* Throws an {@link UnsupportedOperationException} since the type of annotation
* represented by the {@link #getAnnotationAttributes AnnotationAttributes} in
* an {@code UntypedAnnotationDescriptor} is unknown.
* @since 4.2
*/
@Override
public Annotation synthesizeAnnotation() {
throw new UnsupportedOperationException(
"getMergedAnnotation() is unsupported in UntypedAnnotationDescriptor");
}
}
}