From 25354690995aac0c5b9a9502b01b573423635091 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 23 Mar 2016 20:34:43 +0100 Subject: [PATCH] Support repeatable annotations as composed annotations Prior to this commit, AnnotationUtils supported searching for repeatable annotations even if the repeatable annotation was declared on a custom stereotype annotation. However, there was no support for merging of attributes in composed repeatable annotations. In other words, it was not possible for a custom annotation to override attributes in a repeatable annotation. This commit addresses this by introducing findMergedRepeatableAnnotations() methods in AnnotatedElementUtils. These new methods provide full support for explicit annotation attribute overrides configured via @AliasFor (as well as convention-based overrides) with "find semantics". Issue: SPR-13973 --- .../annotation/AnnotatedElementUtils.java | 273 +++++++++++++++--- .../core/annotation/AnnotationUtils.java | 41 +-- .../ComposedRepeatableAnnotationsTests.java | 230 +++++++++++++++ 3 files changed, 487 insertions(+), 57 deletions(-) create mode 100644 spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index 0caaa805e70..dd10bd53489 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -33,8 +33,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** - * General utility methods for finding annotations and meta-annotations on - * {@link AnnotatedElement AnnotatedElements}. + * General utility methods for finding annotations, meta-annotations, and + * repeatable annotations on {@link AnnotatedElement AnnotatedElements}. * *

{@code AnnotatedElementUtils} defines the public API for Spring's * meta-annotation programming model with support for annotation attribute @@ -48,7 +48,8 @@ import org.springframework.util.MultiValueMap; *

Support for meta-annotations with attribute overrides in * composed annotations is provided by all variants of the * {@code getMergedAnnotationAttributes()}, {@code getMergedAnnotation()}, - * {@code findMergedAnnotationAttributes()}, and {@code findMergedAnnotation()} + * {@code findMergedAnnotationAttributes()}, {@code findMergedAnnotation()}, + * {@code findAllMergedAnnotations()}, and {@code findMergedRepeatableAnnotations()} * methods. * *

Find vs. Get Semantics

@@ -96,6 +97,7 @@ public class AnnotatedElementUtils { private static final Boolean CONTINUE = null; + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; /** * Build an adapted {@link AnnotatedElement} for the given annotations, @@ -266,6 +268,7 @@ public class AnnotatedElementUtils { * @param annotationType the annotation type to find * @return {@code true} if a matching annotation is present * @since 4.2.3 + * @see #hasAnnotation(AnnotatedElement, Class) */ public static boolean isAnnotated(AnnotatedElement element, final Class annotationType) { Assert.notNull(element, "AnnotatedElement must not be null"); @@ -442,6 +445,7 @@ public class AnnotatedElementUtils { * @param annotationType the annotation type to find * @return {@code true} if a matching annotation is present * @since 4.3 + * @see #isAnnotated(AnnotatedElement, Class) */ public static boolean hasAnnotation(AnnotatedElement element, final Class annotationType) { Assert.notNull(element, "AnnotatedElement must not be null"); @@ -470,6 +474,8 @@ public class AnnotatedElementUtils { * the result back into an annotation of the specified {@code annotationType}. *

{@link AliasFor @AliasFor} semantics are fully supported, both * within a single annotation and within the annotation hierarchy. + *

This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element * @param annotationType the annotation type to find * @return the merged, synthesized {@code Annotation}, or {@code null} if not found @@ -507,6 +513,8 @@ public class AnnotatedElementUtils { *

This method delegates to {@link #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)} * (supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}) * and {@link AnnotationUtils#synthesizeAnnotation(Map, Class, AnnotatedElement)}. + *

This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element * @param annotationName the fully qualified class name of the annotation type to find * @return the merged, synthesized {@code Annotation}, or {@code null} if not found @@ -528,10 +536,12 @@ public class AnnotatedElementUtils { * within the annotation hierarchy above the supplied {@code element}; * and for each annotation found, merge that annotation's attributes with * matching attributes from annotations in lower levels of the annotation - * hierarchy, and synthesize the result back into an annotation of the specified + * hierarchy and synthesize the result back into an annotation of the specified * {@code annotationType}. *

{@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within the annotation hierarchy. + * single annotation and within annotation hierarchies. + *

This method follows find semantics 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, synthesized {@code Annotations} found, or an empty @@ -557,6 +567,93 @@ public class AnnotatedElementUtils { return annotations; } + /** + * Find all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the result back into an annotation of the specified + * {@code annotationType}. + *

The container type that holds the repeatable annotations will be looked up + * via {@link java.lang.annotation.Repeatable}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows find semantics 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 #findMergedAnnotation(AnnotatedElement, Class) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + * @see #findMergedRepeatableAnnotations(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 Set findMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType) { + + return findMergedRepeatableAnnotations(element, annotationType, null); + } + + /** + * Find all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the result back into an annotation of the specified + * {@code annotationType}. + *

{@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

This method follows find semantics 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 #findMergedAnnotation(AnnotatedElement, Class) + * @see #findAllMergedAnnotations(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 Set findMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType, Class containerType) { + + Assert.notNull(element, "AnnotatedElement must not be null"); + Assert.notNull(annotationType, "annotationType must not be null"); + + if (containerType == null) { + containerType = AnnotationUtils.resolveContainerAnnotationType(annotationType); + if (containerType == null) { + throw new IllegalArgumentException( + "annotationType must be a repeatable annotation: failed to resolve container type for " + + annotationType.getName()); + } + } + else { + validateRepeatableContainerType(annotationType, containerType); + } + + MergedAnnotationAttributesProcessor processor = new MergedAnnotationAttributesProcessor(annotationType, null, + false, false, true); + + searchWithFindSemantics(element, annotationType, annotationType.getName(), containerType, processor); + + Set annotations = new LinkedHashSet(); + for (AnnotationAttributes attributes : processor.getAggregatedResults()) { + AnnotationUtils.postProcessAnnotationAttributes(element, attributes, false, false); + annotations.add(AnnotationUtils.synthesizeAnnotation(attributes, annotationType, element)); + } + return annotations; + } + /** * Find the first annotation of the specified {@code annotationType} within * the annotation hierarchy above the supplied {@code element} and @@ -853,12 +950,37 @@ public class AnnotatedElementUtils { * @return the result of the processor, potentially {@code null} * @since 4.2 */ - private static T searchWithFindSemantics( - AnnotatedElement element, Class annotationType, String annotationName, Processor processor) { + private static T searchWithFindSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Processor processor) { + + return searchWithFindSemantics(element, annotationType, annotationName, null, processor); + } + + /** + * Search for annotations of the specified {@code annotationName} or + * {@code annotationType} on the specified {@code element}, following + * find semantics. + * @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.2 + */ + private static T searchWithFindSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Class containerType, Processor processor) { + + if (containerType != null && !processor.aggregates()) { + throw new IllegalArgumentException( + "Searches for repeatable annotations must supply an aggregating Processor"); + } try { return searchWithFindSemantics( - element, annotationType, annotationName, processor, new HashSet(), 0); + element, annotationType, annotationName, containerType, processor, new HashSet(), 0); } catch (Throwable ex) { AnnotationUtils.rethrowAnnotationConfigurationException(ex); @@ -876,6 +998,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 @@ -883,7 +1007,8 @@ public class AnnotatedElementUtils { * @since 4.2 */ private static T searchWithFindSemantics(AnnotatedElement element, Class annotationType, - String annotationName, Processor processor, Set visited, int metaDepth) { + String annotationName, Class containerType, Processor processor, + Set visited, int metaDepth) { Assert.notNull(element, "AnnotatedElement must not be null"); Assert.hasLength(annotationName, "annotationName must not be null or empty"); @@ -896,17 +1021,29 @@ public class AnnotatedElementUtils { // Search in local annotations for (Annotation annotation : annotations) { - if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation) && - ((annotationType != null ? annotation.annotationType() == annotationType : - annotation.annotationType().getName().equals(annotationName)) || - metaDepth > 0)) { - T result = processor.process(element, annotation, metaDepth); - if (result != null) { - if (processor.aggregates() && metaDepth == 0) { - aggregatedResults.add(result); + if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { + if (annotation.annotationType() == annotationType + || annotation.annotationType().getName().equals(annotationName)) { + + T result = processor.process(element, annotation, metaDepth); + if (result != null) { + if (processor.aggregates() && metaDepth == 0) { + aggregatedResults.add(result); + } + else { + return result; + } } - else { - return result; + } + // Repeatable annotations in container? + else if (annotation.annotationType() == containerType) { + for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) { + T result = processor.process(element, contained, metaDepth); + if (result != null) { + // No need to post-process since repeatable annotations within a + // container cannot be composed annotations. + aggregatedResults.add(result); + } } } } @@ -915,8 +1052,8 @@ public class AnnotatedElementUtils { // Search in meta annotations on local annotations for (Annotation annotation : annotations) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { - T result = searchWithFindSemantics( - annotation.annotationType(), annotationType, annotationName, processor, visited, metaDepth + 1); + T result = searchWithFindSemantics(annotation.annotationType(), annotationType, annotationName, + containerType, processor, visited, metaDepth + 1); if (result != null) { processor.postProcess(annotation.annotationType(), annotation, result); if (processor.aggregates() && metaDepth == 0) { @@ -930,6 +1067,7 @@ public class AnnotatedElementUtils { } if (processor.aggregates()) { + // Prepend to support top-down ordering within class hierarchies processor.getAggregatedResults().addAll(0, aggregatedResults); } @@ -938,7 +1076,7 @@ public class AnnotatedElementUtils { // Search on possibly bridged method Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); - T result = searchWithFindSemantics(resolvedMethod, annotationType, annotationName, + T result = searchWithFindSemantics(resolvedMethod, annotationType, annotationName, containerType, processor, visited, metaDepth); if (result != null) { return result; @@ -946,8 +1084,8 @@ public class AnnotatedElementUtils { // Search on methods in interfaces declared locally Class[] ifcs = method.getDeclaringClass().getInterfaces(); - result = searchOnInterfaces( - method, annotationType, annotationName, processor, visited, metaDepth, ifcs); + result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor, + visited, metaDepth, ifcs); if (result != null) { return result; } @@ -964,7 +1102,7 @@ public class AnnotatedElementUtils { Method equivalentMethod = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); Method resolvedEquivalentMethod = BridgeMethodResolver.findBridgedMethod(equivalentMethod); result = searchWithFindSemantics(resolvedEquivalentMethod, annotationType, annotationName, - processor, visited, metaDepth); + containerType, processor, visited, metaDepth); if (result != null) { return result; } @@ -974,8 +1112,8 @@ public class AnnotatedElementUtils { } // Search on interfaces declared on superclass - result = searchOnInterfaces(method, annotationType, annotationName, processor, visited, - metaDepth, clazz.getInterfaces()); + result = searchOnInterfaces(method, annotationType, annotationName, containerType, processor, + visited, metaDepth, clazz.getInterfaces()); if (result != null) { return result; } @@ -987,8 +1125,8 @@ public class AnnotatedElementUtils { // Search on interfaces for (Class ifc : clazz.getInterfaces()) { - T result = searchWithFindSemantics( - ifc, annotationType, annotationName, processor, visited, metaDepth); + T result = searchWithFindSemantics(ifc, annotationType, annotationName, containerType, + processor, visited, metaDepth); if (result != null) { return result; } @@ -997,8 +1135,8 @@ public class AnnotatedElementUtils { // Search on superclass Class superclass = clazz.getSuperclass(); if (superclass != null && Object.class != superclass) { - T result = searchWithFindSemantics( - superclass, annotationType, annotationName, processor, visited, metaDepth); + T result = searchWithFindSemantics(superclass, annotationType, annotationName, containerType, + processor, visited, metaDepth); if (result != null) { return result; } @@ -1012,14 +1150,15 @@ public class AnnotatedElementUtils { return null; } - private static T searchOnInterfaces(Method method, Class annotationType, String annotationName, - Processor processor, Set visited, int metaDepth, Class[] ifcs) { + private static T searchOnInterfaces(Method method, Class annotationType, + String annotationName, Class containerType, Processor processor, + Set visited, int metaDepth, Class[] ifcs) { for (Class iface : ifcs) { if (AnnotationUtils.isInterfaceWithAnnotatedMethods(iface)) { try { Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes()); - T result = searchWithFindSemantics(equivalentMethod, annotationType, annotationName, + T result = searchWithFindSemantics(equivalentMethod, annotationType, annotationName, containerType, processor, visited, metaDepth); if (result != null) { return result; @@ -1034,6 +1173,55 @@ public class AnnotatedElementUtils { return null; } + /** + * Get the array of raw (unsynthesized) annotations from the {@code value} + * attribute of the supplied repeatable annotation {@code container}. + * @since 4.3 + */ + @SuppressWarnings("unchecked") + private static A[] getRawAnnotationsFromContainer(AnnotatedElement element, + Annotation container) { + + try { + return (A[]) AnnotationUtils.getValue(container); + } + catch (Exception ex) { + AnnotationUtils.handleIntrospectionFailure(element, ex); + } + // Unable to read value from repeating annotation container -> ignore it. + return (A[]) EMPTY_ANNOTATION_ARRAY; + } + + /** + * Validate that the supplied {@code containerType} is a proper container + * annotation for the supplied repeatable {@code annotationType} (i.e., + * that it declares a {@code value} attribute that holds an array of the + * {@code annotationType}). + * @since 4.3 + * @throws AnnotationConfigurationException if the supplied {@code containerType} + * is not a valid container annotation for the supplied {@code annotationType} + */ + private static void validateRepeatableContainerType(Class annotationType, + Class containerType) { + + try { + Method method = containerType.getDeclaredMethod(AnnotationUtils.VALUE); + Class returnType = method.getReturnType(); + if (!returnType.isArray() || returnType.getComponentType() != annotationType) { + String msg = String.format( + "Container type [%s] must declare a 'value' attribute for an array of type [%s]", + containerType.getName(), annotationType.getName()); + throw new AnnotationConfigurationException(msg); + } + } + catch (Exception ex) { + AnnotationUtils.rethrowAnnotationConfigurationException(ex); + String msg = String.format("Invalid declaration of container type [%s] for repeatable annotation [%s]", + containerType.getName(), annotationType.getName()); + throw new AnnotationConfigurationException(msg, ex); + } + } + /** * Callback interface that is used to process annotations during a search. @@ -1114,10 +1302,10 @@ public class AnnotatedElementUtils { /** * Get the list of results aggregated by this processor. - *

NOTE: the processor does not aggregate the results itself. - * Rather, the search algorithm that uses this processor is responsible - * for asking this processor if it {@link #aggregates} results and then - * adding the post-processed results to the list returned by this + *

NOTE: the processor does not aggregate the results + * itself. Rather, the search algorithm that uses this processor is + * responsible for asking this processor if it {@link #aggregates} results + * and then adding the post-processed results to the list returned by this * method. *

WARNING: aggregation is currently only supported for find semantics. * @return the list of results aggregated by this processor; never @@ -1158,6 +1346,8 @@ public class AnnotatedElementUtils { * target annotation during the {@link #process} phase and then merges * annotation attributes from lower levels in the annotation hierarchy * during the {@link #postProcess} phase. + *

A {@code MergedAnnotationAttributesProcessor} may optionally be + * configured to {@linkplain #aggregates aggregate} results. * @since 4.2 * @see AnnotationUtils#retrieveAnnotationAttributes * @see AnnotationUtils#postProcessAnnotationAttributes @@ -1172,6 +1362,8 @@ public class AnnotatedElementUtils { private final boolean nestedAnnotationsAsMap; + private final boolean aggregates; + private final List aggregatedResults; @@ -1188,12 +1380,13 @@ public class AnnotatedElementUtils { this.annotationName = annotationName; this.classValuesAsString = classValuesAsString; this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; + this.aggregates = aggregates; this.aggregatedResults = (aggregates ? new ArrayList() : null); } @Override public boolean aggregates() { - return this.aggregatedResults != null; + return this.aggregates; } @Override @@ -1232,7 +1425,7 @@ public class AnnotatedElementUtils { targetAttributeNames.add(attributeOverrideName); valuesAlreadyReplaced.add(attributeOverrideName); - // Ensure all aliased attributes in the target annotation are also overridden. (SPR-14069) + // Ensure all aliased attributes in the target annotation are overridden. (SPR-14069) List aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName); if (aliases != null) { for (String alias : aliases) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 94a994f4ba3..a06fe2f365d 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -111,6 +111,7 @@ public abstract class AnnotationUtils { */ public static final String VALUE = "value"; + private static final String REPEATABLE_CLASS_NAME = "java.lang.annotation.Repeatable"; private static final Map findAnnotationCache = new ConcurrentReferenceHashMap(256); @@ -1720,6 +1721,29 @@ public abstract class AnnotationUtils { return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0); } + /** + * Resolve the container type for the supplied repeatable {@code annotationType}. + *

Automatically detects a container annotation declared via + * {@link java.lang.annotation.Repeatable}. If the supplied annotation type + * is not annotated with {@code @Repeatable}, this method simply returns + * {@code null}. + * @since 4.2 + */ + @SuppressWarnings("unchecked") + static Class resolveContainerAnnotationType(Class annotationType) { + try { + Annotation repeatable = getAnnotation(annotationType, REPEATABLE_CLASS_NAME); + if (repeatable != null) { + Object value = AnnotationUtils.getValue(repeatable); + return (Class) value; + } + } + catch (Exception ex) { + handleIntrospectionFailure(annotationType, ex); + } + return null; + } + /** *

If the supplied throwable is an {@link AnnotationConfigurationException}, * it will be cast to an {@code AnnotationConfigurationException} and thrown, @@ -1806,8 +1830,6 @@ public abstract class AnnotationUtils { private static class AnnotationCollector { - private static final String REPEATABLE_CLASS_NAME = "java.lang.annotation.Repeatable"; - private final Class annotationType; private final Class containerAnnotationType; @@ -1825,21 +1847,6 @@ public abstract class AnnotationUtils { this.declaredMode = declaredMode; } - @SuppressWarnings("unchecked") - static Class resolveContainerAnnotationType(Class annotationType) { - try { - Annotation repeatable = getAnnotation(annotationType, REPEATABLE_CLASS_NAME); - if (repeatable != null) { - Object value = AnnotationUtils.getValue(repeatable); - return (Class) value; - } - } - catch (Exception ex) { - handleIntrospectionFailure(annotationType, ex); - } - return null; - } - Set getResult(AnnotatedElement element) { process(element); return Collections.unmodifiableSet(this.result); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java new file mode 100644 index 00000000000..e532e5e571a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2016 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.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.util.Iterator; +import java.util.Set; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.CoreMatchers.isA; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.*; +import static org.springframework.core.annotation.AnnotatedElementUtils.*; + +/** + * Unit tests that verify support for finding all composed, repeatable + * annotations on a single annotated element. + * + *

See SPR-13973. + * + * @author Sam Brannen + * @since 4.3 + * @see AnnotatedElementUtils + * @see AnnotatedElementUtilsTests + */ +public class ComposedRepeatableAnnotationsTests { + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + + @Test + public void nonRepeatableAnnotation() { + 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())); + findMergedRepeatableAnnotations(getClass(), NonRepeatable.class); + } + + @Test + public void invalidRepeatableAnnotationContainerMissingValueAttribute() { + 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)); + findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class); + } + + @Test + public void invalidRepeatableAnnotationContainerWithNonArrayValueAttribute() { + 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())); + findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class); + } + + @Test + public void invalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() { + 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())); + findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, + ContainerWithArrayValueAttributeButWrongComponentType.class); + } + + @Test + public void repeatableAnnotationsOnClass() { + assertRepeatableAnnotations(RepeatableClass.class); + } + + @Test + public void repeatableAnnotationsOnSuperclass() { + assertRepeatableAnnotations(SubRepeatableClass.class); + } + + @Test + public void composedRepeatableAnnotationsOnClass() { + assertRepeatableAnnotations(ComposedRepeatableClass.class); + } + + @Test + public void composedRepeatableAnnotationsMixedWithContainerOnClass() { + assertRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class); + } + + @Test + public void composedContainerForRepeatableAnnotationsOnClass() { + assertRepeatableAnnotations(ComposedContainerClass.class); + } + + private void assertRepeatableAnnotations(AnnotatedElement element) { + assertNotNull(element); + + Set peteRepeats = findMergedRepeatableAnnotations(element, PeteRepeat.class); + assertNotNull(peteRepeats); + assertEquals(3, peteRepeats.size()); + + Iterator iterator = peteRepeats.iterator(); + assertEquals("A", iterator.next().value()); + assertEquals("B", iterator.next().value()); + assertEquals("C", iterator.next().value()); + } + + + // ------------------------------------------------------------------------- + + @Retention(RetentionPolicy.RUNTIME) + @interface NonRepeatable { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerMissingValueAttribute { + // InvalidRepeatable[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithNonArrayValueAttribute { + + InvalidRepeatable value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithArrayValueAttributeButWrongComponentType { + + String[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface InvalidRepeatable { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface PeteRepeats { + + PeteRepeat[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Repeatable(PeteRepeats.class) + @interface PeteRepeat { + + String value(); + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForPetesSake { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForTheLoveOfFoo { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + } + + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ComposedContainer { + } + + @PeteRepeat("A") + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + static class RepeatableClass { + } + + static class SubRepeatableClass extends RepeatableClass { + } + + @ForPetesSake("B") + @ForTheLoveOfFoo("C") + @PeteRepeat("A") + static class ComposedRepeatableClass { + } + + @ForPetesSake("C") + @PeteRepeats(@PeteRepeat("A")) + @PeteRepeat("B") + static class ComposedRepeatableMixedWithContainerClass { + } + + @PeteRepeat("A") + @ComposedContainer + static class ComposedContainerClass { + } + +}