Support searches for merged repeatable annotations with "get" semantics

This commit picks up where 2535469099 left off with added support for
"get" search semantics for merged repeatable annotations.

Specifically, this commit introduces a new
getMergedRepeatableAnnotations() method in AnnotatedElementUtils.

Issue: SPR-13973
This commit is contained in:
Sam Brannen 2016-03-24 19:23:32 +01:00
parent 8d0083ca96
commit 46e0484bf8
4 changed files with 325 additions and 46 deletions

View File

@ -48,6 +48,7 @@ import org.springframework.util.MultiValueMap;
* <p>Support for meta-annotations with <em>attribute overrides</em> in
* <em>composed annotations</em> is provided by all variants of the
* {@code getMergedAnnotationAttributes()}, {@code getMergedAnnotation()},
* {@code getAllMergedAnnotations()}, {@code getMergedRepeatableAnnotations()},
* {@code findMergedAnnotationAttributes()}, {@code findMergedAnnotation()},
* {@code findAllMergedAnnotations()}, and {@code findMergedRepeatableAnnotations()}
* methods.
@ -150,7 +151,8 @@ public class AnnotatedElementUtils {
try {
Annotation annotation = element.getAnnotation(annotationType);
if (annotation != null) {
searchWithGetSemantics(annotation.annotationType(), annotationType, null, new SimpleAnnotationProcessor<Object>() {
searchWithGetSemantics(annotation.annotationType(), annotationType, null, null,
new SimpleAnnotationProcessor<Object>() {
@Override
public Object process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) {
types.add(annotation.annotationType().getName());
@ -189,7 +191,8 @@ public class AnnotatedElementUtils {
try {
Annotation annotation = AnnotationUtils.getAnnotation(element, annotationName);
if (annotation != null) {
searchWithGetSemantics(annotation.annotationType(), null, annotationName, new SimpleAnnotationProcessor<Object>() {
searchWithGetSemantics(annotation.annotationType(), null, annotationName, null,
new SimpleAnnotationProcessor<Object>() {
@Override
public Object process(AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) {
types.add(annotation.annotationType().getName());
@ -409,6 +412,7 @@ public class AnnotatedElementUtils {
public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element,
String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
Assert.hasLength(annotationName, "annotationName must not be null or empty");
AnnotationAttributes attributes = searchWithGetSemantics(element, null, annotationName,
new MergedAnnotationAttributesProcessor(null, annotationName, classValuesAsString, nestedAnnotationsAsMap));
AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap);
@ -483,6 +487,81 @@ public class AnnotatedElementUtils {
return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults());
}
/**
* Get all <em>repeatable annotations</em> of the specified {@code annotationType}
* within the annotation hierarchy <em>above</em> the supplied {@code element};
* and for each annotation found, merge that annotation's attributes with
* <em>matching</em> attributes from annotations in lower levels of the annotation
* hierarchy and synthesize the results back into an annotation of the specified
* {@code annotationType}.
* <p>The container type that holds the repeatable annotations will be looked up
* via {@link java.lang.annotation.Repeatable}.
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a
* single annotation and within annotation hierarchies.
* <p>This method follows <em>get semantics</em> as described in the
* {@linkplain AnnotatedElementUtils class-level javadoc}.
* @param element the annotated element; never {@code null}
* @param annotationType the annotation type to find; never {@code null}
* @return the set of all merged repeatable {@code Annotations} found, or an empty
* set if none were found
* @since 4.3
* @see #getMergedAnnotation(AnnotatedElement, Class)
* @see #getAllMergedAnnotations(AnnotatedElement, Class)
* @see #getMergedRepeatableAnnotations(AnnotatedElement, Class, Class)
* @throws IllegalArgumentException if the {@code element} or {@code annotationType}
* is {@code null}, or if the container type cannot be resolved
*/
public static <A extends Annotation> Set<A> getMergedRepeatableAnnotations(AnnotatedElement element,
Class<A> annotationType) {
return getMergedRepeatableAnnotations(element, annotationType, null);
}
/**
* Get all <em>repeatable annotations</em> of the specified {@code annotationType}
* within the annotation hierarchy <em>above</em> the supplied {@code element};
* and for each annotation found, merge that annotation's attributes with
* <em>matching</em> attributes from annotations in lower levels of the annotation
* hierarchy and synthesize the results back into an annotation of the specified
* {@code annotationType}.
* <p>{@link AliasFor @AliasFor} semantics are fully supported, both within a
* single annotation and within annotation hierarchies.
* <p>This method follows <em>get semantics</em> as described in the
* {@linkplain AnnotatedElementUtils class-level javadoc}.
* @param element the annotated element; never {@code null}
* @param annotationType the annotation type to find; never {@code null}
* @param containerType the type of the container that holds the annotations;
* may be {@code null} if the container type should be looked up via
* {@link java.lang.annotation.Repeatable}
* @return the set of all merged repeatable {@code Annotations} found, or an empty
* set if none were found
* @since 4.3
* @see #getMergedAnnotation(AnnotatedElement, Class)
* @see #getAllMergedAnnotations(AnnotatedElement, Class)
* @throws IllegalArgumentException if the {@code element} or {@code annotationType}
* is {@code null}, or if the container type cannot be resolved
* @throws AnnotationConfigurationException if the supplied {@code containerType}
* is not a valid container annotation for the supplied {@code annotationType}
*/
public static <A extends Annotation> Set<A> getMergedRepeatableAnnotations(AnnotatedElement element,
Class<A> annotationType, Class<? extends Annotation> containerType) {
Assert.notNull(element, "AnnotatedElement must not be null");
Assert.notNull(annotationType, "annotationType must not be null");
if (containerType == null) {
containerType = resolveContainerType(annotationType);
}
else {
validateRepeatableContainerType(annotationType, containerType);
}
MergedAnnotationAttributesProcessor processor =
new MergedAnnotationAttributesProcessor(annotationType, null, false, false, true);
searchWithGetSemantics(element, annotationType, null, containerType, processor);
return postProcessAndSynthesizeAggregatedResults(element, annotationType, processor.getAggregatedResults());
}
/**
* Get the annotation attributes of <strong>all</strong> annotations of the specified
* {@code annotationName} in the annotation hierarchy above the supplied
@ -832,12 +911,32 @@ public class AnnotatedElementUtils {
* @param processor the processor to delegate to
* @return the result of the processor, potentially {@code null}
*/
private static <T> T searchWithGetSemantics(AnnotatedElement element,
Class<? extends Annotation> annotationType, String annotationName, Processor<T> processor) {
private static <T> T searchWithGetSemantics(AnnotatedElement element, Class<? extends Annotation> annotationType,
String annotationName, Processor<T> processor) {
return searchWithGetSemantics(element, annotationType, annotationName, null, processor);
}
/**
* Search for annotations of the specified {@code annotationName} or
* {@code annotationType} on the specified {@code element}, following
* <em>get semantics</em>.
* @param element the annotated element
* @param annotationType the annotation type to find
* @param annotationName the fully qualified class name of the annotation
* type to find (as an alternative to {@code annotationType})
* @param containerType the type of the container that holds repeatable
* annotations, or {@code null} if the annotation is not repeatable
* @param processor the processor to delegate to
* @return the result of the processor, potentially {@code null}
* @since 4.3
*/
private static <T> T searchWithGetSemantics(AnnotatedElement element, Class<? extends Annotation> annotationType,
String annotationName, Class<? extends Annotation> containerType, Processor<T> processor) {
try {
return searchWithGetSemantics(
element, annotationType, annotationName, processor, new HashSet<AnnotatedElement>(), 0);
return searchWithGetSemantics(element, annotationType, annotationName, containerType, processor,
new HashSet<AnnotatedElement>(), 0);
}
catch (Throwable ex) {
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
@ -855,14 +954,16 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find
* @param annotationName the fully qualified class name of the annotation
* type to find (as an alternative to {@code annotationType})
* @param containerType the type of the container that holds repeatable
* annotations, or {@code null} if the annotation is not repeatable
* @param processor the processor to delegate to
* @param visited the set of annotated elements that have already been visited
* @param metaDepth the meta-depth of the annotation
* @return the result of the processor, potentially {@code null}
*/
private static <T> T searchWithGetSemantics(AnnotatedElement element,
Class<? extends Annotation> annotationType, String annotationName,
Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {
private static <T> T searchWithGetSemantics(AnnotatedElement element, Class<? extends Annotation> annotationType,
String annotationName, Class<? extends Annotation> containerType, Processor<T> processor,
Set<AnnotatedElement> visited, int metaDepth) {
Assert.notNull(element, "AnnotatedElement must not be null");
@ -871,12 +972,12 @@ public class AnnotatedElementUtils {
// Start searching within locally declared annotations
List<Annotation> declaredAnnotations = Arrays.asList(element.getDeclaredAnnotations());
T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations,
annotationType, annotationName, processor, visited, metaDepth);
annotationType, annotationName, containerType, processor, visited, metaDepth);
if (result != null) {
return result;
}
if (element instanceof Class) { // otherwise getAnnotations doesn't return anything new
if (element instanceof Class) { // otherwise getAnnotations doesn't return anything new
List<Annotation> inheritedAnnotations = new ArrayList<Annotation>();
for (Annotation annotation : element.getAnnotations()) {
if (!declaredAnnotations.contains(annotation)) {
@ -886,7 +987,7 @@ public class AnnotatedElementUtils {
// Continue searching within inherited annotations
result = searchWithGetSemanticsInAnnotations(element, inheritedAnnotations,
annotationType, annotationName, processor, visited, metaDepth);
annotationType, annotationName, containerType, processor, visited, metaDepth);
if (result != null) {
return result;
}
@ -915,6 +1016,8 @@ public class AnnotatedElementUtils {
* @param annotationType the annotation type to find
* @param annotationName the fully qualified class name of the annotation
* type to find (as an alternative to {@code annotationType})
* @param containerType the type of the container that holds repeatable
* annotations, or {@code null} if the annotation is not repeatable
* @param processor the processor to delegate to
* @param visited the set of annotated elements that have already been visited
* @param metaDepth the meta-depth of the annotation
@ -923,21 +1026,39 @@ public class AnnotatedElementUtils {
*/
private static <T> T searchWithGetSemanticsInAnnotations(AnnotatedElement annotatedElement,
List<Annotation> annotations, Class<? extends Annotation> annotationType, String annotationName,
Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {
Class<? extends Annotation> containerType, Processor<T> processor, Set<AnnotatedElement> visited,
int metaDepth) {
// Search in annotations
for (Annotation annotation : annotations) {
// Note: we only check for (metaDepth > 0) due to the nuances of getMetaAnnotationTypes().
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) &&
((annotation.annotationType() == annotationType
|| annotation.annotationType().getName().equals(annotationName)) || metaDepth > 0)) {
T result = processor.process(annotatedElement, annotation, metaDepth);
if (result != null) {
if (processor.aggregates() && metaDepth == 0) {
processor.getAggregatedResults().add(result);
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
// TODO Check non-repeatable annotations first, once we have sorted out
// the metaDepth nuances of getMetaAnnotationTypes().
// Repeatable annotations in container?
if (annotation.annotationType() == containerType) {
for (Annotation contained : getRawAnnotationsFromContainer(annotatedElement, annotation)) {
T result = processor.process(annotatedElement, contained, metaDepth);
if (result != null) {
// No need to post-process since repeatable annotations within a
// container cannot be composed annotations.
processor.getAggregatedResults().add(result);
}
}
else {
return result;
}
else if ((annotation.annotationType() == annotationType
|| annotation.annotationType().getName().equals(annotationName)) || metaDepth > 0) {
// Note: we only check for (metaDepth > 0) due to the nuances of getMetaAnnotationTypes().
T result = processor.process(annotatedElement, annotation, metaDepth);
if (result != null) {
if (processor.aggregates() && metaDepth == 0) {
processor.getAggregatedResults().add(result);
}
else {
return result;
}
}
}
}
@ -947,7 +1068,7 @@ public class AnnotatedElementUtils {
for (Annotation annotation : annotations) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
T result = searchWithGetSemantics(annotation.annotationType(), annotationType,
annotationName, processor, visited, metaDepth + 1);
annotationName, containerType, processor, visited, metaDepth + 1);
if (result != null) {
processor.postProcess(annotatedElement, annotation, result);
if (processor.aggregates() && metaDepth == 0) {

View File

@ -57,6 +57,7 @@ import static org.springframework.core.annotation.AnnotationUtilsTests.*;
* @since 4.0.3
* @see AnnotationUtilsTests
* @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests
* @see ComposedRepeatableAnnotationsTests
*/
public class AnnotatedElementUtilsTests {

View File

@ -37,15 +37,17 @@ import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotatedElementUtils.*;
/**
* Unit tests that verify support for finding all composed, repeatable
* Unit tests that verify support for getting and finding all composed, repeatable
* annotations on a single annotated element.
*
* <p>See <a href="https://jira.spring.io/browse/SPR-13973">SPR-13973</a>.
*
* @author Sam Brannen
* @since 4.3
* @see AnnotatedElementUtils
* @see AnnotatedElementUtils#getMergedRepeatableAnnotations
* @see AnnotatedElementUtils#findMergedRepeatableAnnotations
* @see AnnotatedElementUtilsTests
* @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests
*/
public class ComposedRepeatableAnnotationsTests {
@ -53,43 +55,92 @@ public class ComposedRepeatableAnnotationsTests {
public final ExpectedException exception = ExpectedException.none();
@Test
public void getNonRepeatableAnnotation() {
expectNonRepeatableAnnotation();
getMergedRepeatableAnnotations(getClass(), NonRepeatable.class);
}
@Test
public void getInvalidRepeatableAnnotationContainerMissingValueAttribute() {
expectContainerMissingValueAttribute();
getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class);
}
@Test
public void getInvalidRepeatableAnnotationContainerWithNonArrayValueAttribute() {
expectContainerWithNonArrayValueAttribute();
getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class);
}
@Test
public void getInvalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() {
expectContainerWithArrayValueAttributeButWrongComponentType();
getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class,
ContainerWithArrayValueAttributeButWrongComponentType.class);
}
@Test
public void getRepeatableAnnotationsOnClass() {
assertGetRepeatableAnnotations(RepeatableClass.class);
}
@Test
public void getRepeatableAnnotationsOnSuperclass() {
assertGetRepeatableAnnotations(SubRepeatableClass.class);
}
@Test
public void getComposedRepeatableAnnotationsOnClass() {
assertGetRepeatableAnnotations(ComposedRepeatableClass.class);
}
@Test
public void getComposedRepeatableAnnotationsMixedWithContainerOnClass() {
assertGetRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class);
}
@Test
public void getComposedContainerForRepeatableAnnotationsOnClass() {
assertGetRepeatableAnnotations(ComposedContainerClass.class);
}
@Test
public void getNoninheritedComposedRepeatableAnnotationsOnClass() {
Class<?> element = NoninheritedRepeatableClass.class;
Set<Noninherited> annotations = getMergedRepeatableAnnotations(element, Noninherited.class);
assertNoninheritedRepeatableAnnotations(annotations);
}
@Test
public void getNoninheritedComposedRepeatableAnnotationsOnSuperclass() {
Class<?> element = SubNoninheritedRepeatableClass.class;
Set<Noninherited> annotations = getMergedRepeatableAnnotations(element, Noninherited.class);
assertNotNull(annotations);
assertEquals(0, annotations.size());
}
@Test
public void findNonRepeatableAnnotation() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("annotationType must be a repeatable annotation"));
exception.expectMessage(containsString("failed to resolve container type for"));
exception.expectMessage(containsString(NonRepeatable.class.getName()));
expectNonRepeatableAnnotation();
findMergedRepeatableAnnotations(getClass(), NonRepeatable.class);
}
@Test
public void findInvalidRepeatableAnnotationContainerMissingValueAttribute() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Invalid declaration of container type"));
exception.expectMessage(containsString(ContainerMissingValueAttribute.class.getName()));
exception.expectMessage(containsString("for repeatable annotation"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
exception.expectCause(isA(NoSuchMethodException.class));
expectContainerMissingValueAttribute();
findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class);
}
@Test
public void findInvalidRepeatableAnnotationContainerWithNonArrayValueAttribute() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Container type"));
exception.expectMessage(containsString(ContainerWithNonArrayValueAttribute.class.getName()));
exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
expectContainerWithNonArrayValueAttribute();
findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class);
}
@Test
public void findInvalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Container type"));
exception.expectMessage(containsString(ContainerWithArrayValueAttributeButWrongComponentType.class.getName()));
exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
expectContainerWithArrayValueAttributeButWrongComponentType();
findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class,
ContainerWithArrayValueAttributeButWrongComponentType.class);
}
@ -114,11 +165,70 @@ public class ComposedRepeatableAnnotationsTests {
assertFindRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class);
}
@Test
public void findNoninheritedComposedRepeatableAnnotationsOnClass() {
Class<?> element = NoninheritedRepeatableClass.class;
Set<Noninherited> annotations = findMergedRepeatableAnnotations(element, Noninherited.class);
assertNoninheritedRepeatableAnnotations(annotations);
}
@Test
public void findNoninheritedComposedRepeatableAnnotationsOnSuperclass() {
Class<?> element = SubNoninheritedRepeatableClass.class;
Set<Noninherited> annotations = findMergedRepeatableAnnotations(element, Noninherited.class);
assertNoninheritedRepeatableAnnotations(annotations);
}
@Test
public void findComposedContainerForRepeatableAnnotationsOnClass() {
assertFindRepeatableAnnotations(ComposedContainerClass.class);
}
private void expectNonRepeatableAnnotation() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("annotationType must be a repeatable annotation"));
exception.expectMessage(containsString("failed to resolve container type for"));
exception.expectMessage(containsString(NonRepeatable.class.getName()));
}
private void expectContainerMissingValueAttribute() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Invalid declaration of container type"));
exception.expectMessage(containsString(ContainerMissingValueAttribute.class.getName()));
exception.expectMessage(containsString("for repeatable annotation"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
exception.expectCause(isA(NoSuchMethodException.class));
}
private void expectContainerWithNonArrayValueAttribute() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Container type"));
exception.expectMessage(containsString(ContainerWithNonArrayValueAttribute.class.getName()));
exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
}
private void expectContainerWithArrayValueAttributeButWrongComponentType() {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Container type"));
exception.expectMessage(containsString(ContainerWithArrayValueAttributeButWrongComponentType.class.getName()));
exception.expectMessage(containsString("must declare a 'value' attribute for an array of type"));
exception.expectMessage(containsString(InvalidRepeatable.class.getName()));
}
private void assertGetRepeatableAnnotations(AnnotatedElement element) {
assertNotNull(element);
Set<PeteRepeat> peteRepeats = getMergedRepeatableAnnotations(element, PeteRepeat.class);
assertNotNull(peteRepeats);
assertEquals(3, peteRepeats.size());
Iterator<PeteRepeat> iterator = peteRepeats.iterator();
assertEquals("A", iterator.next().value());
assertEquals("B", iterator.next().value());
assertEquals("C", iterator.next().value());
}
private void assertFindRepeatableAnnotations(AnnotatedElement element) {
assertNotNull(element);
@ -132,6 +242,16 @@ public class ComposedRepeatableAnnotationsTests {
assertEquals("C", iterator.next().value());
}
private void assertNoninheritedRepeatableAnnotations(Set<Noninherited> annotations) {
assertNotNull(annotations);
assertEquals(3, annotations.size());
Iterator<Noninherited> iterator = annotations.iterator();
assertEquals("A", iterator.next().value());
assertEquals("B", iterator.next().value());
assertEquals("C", iterator.next().value());
}
// -------------------------------------------------------------------------
@ -227,4 +347,40 @@ public class ComposedRepeatableAnnotationsTests {
static class ComposedContainerClass {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Noninheriteds {
Noninherited[] value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Noninheriteds.class)
@interface Noninherited {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
}
@Noninherited(name = "shadowed")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedNoninherited {
@AliasFor(annotation = Noninherited.class)
String name() default "";
}
@ComposedNoninherited(name = "C")
@Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") })
static class NoninheritedRepeatableClass {
}
static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass {
}
}

View File

@ -42,6 +42,7 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.*;
* @since 4.3
* @see AnnotatedElementUtils
* @see AnnotatedElementUtilsTests
* @see ComposedRepeatableAnnotationsTests
*/
public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {