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 2447039dbe..f223eef91d 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 @@ -48,6 +48,7 @@ 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 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() { + searchWithGetSemantics(annotation.annotationType(), annotationType, null, null, + new SimpleAnnotationProcessor() { @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() { + searchWithGetSemantics(annotation.annotationType(), null, annotationName, null, + new SimpleAnnotationProcessor() { @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 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 results 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 get 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 #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 Set getMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType) { + + return getMergedRepeatableAnnotations(element, annotationType, null); + } + + /** + * Get 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 results 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 get 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 #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 Set getMergedRepeatableAnnotations(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 = 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 all 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 searchWithGetSemantics(AnnotatedElement element, - Class annotationType, String annotationName, Processor processor) { + private static T searchWithGetSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Processor 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 + * get 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.3 + */ + private static T searchWithGetSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Class containerType, Processor processor) { try { - return searchWithGetSemantics( - element, annotationType, annotationName, processor, new HashSet(), 0); + return searchWithGetSemantics(element, annotationType, annotationName, containerType, processor, + new HashSet(), 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 searchWithGetSemantics(AnnotatedElement element, - Class annotationType, String annotationName, - Processor processor, Set visited, int metaDepth) { + private static T searchWithGetSemantics(AnnotatedElement element, Class annotationType, + String annotationName, Class containerType, Processor processor, + Set 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 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 inheritedAnnotations = new ArrayList(); 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 searchWithGetSemanticsInAnnotations(AnnotatedElement annotatedElement, List annotations, Class annotationType, String annotationName, - Processor processor, Set visited, int metaDepth) { + Class containerType, Processor processor, Set 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) { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 09361ee904..20edf59b3d 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -57,6 +57,7 @@ import static org.springframework.core.annotation.AnnotationUtilsTests.*; * @since 4.0.3 * @see AnnotationUtilsTests * @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests + * @see ComposedRepeatableAnnotationsTests */ public class AnnotatedElementUtilsTests { 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 index 2876c55db5..5cbc10d9a8 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java @@ -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. * *

See SPR-13973. * * @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 annotations = getMergedRepeatableAnnotations(element, Noninherited.class); + assertNoninheritedRepeatableAnnotations(annotations); + } + + @Test + public void getNoninheritedComposedRepeatableAnnotationsOnSuperclass() { + Class element = SubNoninheritedRepeatableClass.class; + Set 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 annotations = findMergedRepeatableAnnotations(element, Noninherited.class); + assertNoninheritedRepeatableAnnotations(annotations); + } + + @Test + public void findNoninheritedComposedRepeatableAnnotationsOnSuperclass() { + Class element = SubNoninheritedRepeatableClass.class; + Set 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 peteRepeats = getMergedRepeatableAnnotations(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()); + } + private void assertFindRepeatableAnnotations(AnnotatedElement element) { assertNotNull(element); @@ -132,6 +242,16 @@ public class ComposedRepeatableAnnotationsTests { assertEquals("C", iterator.next().value()); } + private void assertNoninheritedRepeatableAnnotations(Set annotations) { + assertNotNull(annotations); + assertEquals(3, annotations.size()); + + Iterator 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 { + } + } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java index 395b9c31b5..fc2259acf4 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java @@ -42,6 +42,7 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.*; * @since 4.3 * @see AnnotatedElementUtils * @see AnnotatedElementUtilsTests + * @see ComposedRepeatableAnnotationsTests */ public class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {