From f17173f6d5fb167db1c0a6905c23e52caace939b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 10 Aug 2015 14:55:54 +0200 Subject: [PATCH] Synthesize nested maps into annotations Prior to this commit, attempting to synthesize an annotation from a map of annotation attributes that contained nested maps instead of nested annotations would result in an exception. This commit addresses this issue by properly synthesizing nested maps and nested arrays of maps into nested annotations and nested arrays of annotations, respectively. Issue: SPR-13338 --- .../core/annotation/AnnotationUtils.java | 46 ++++++++++-- .../MapAnnotationAttributeExtractor.java | 56 +++++++++++---- .../core/annotation/AnnotationUtilsTests.java | 70 +++++++++++++++++++ 3 files changed, 155 insertions(+), 17 deletions(-) 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 606f76feba..7bd171a17d 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 @@ -1242,7 +1242,9 @@ public abstract class AnnotationUtils { * that are annotated with {@link AliasFor @AliasFor}. *

The supplied map must contain a key-value pair for every attribute * defined in the supplied {@code annotationType} that is not aliased or - * does not have a default value. + * does not have a default value. Nested maps and nested arrays of maps + * will be recursively synthesized into nested annotations or nested + * arrays of annotations, respectively. *

Note that {@link AnnotationAttributes} is a specialized type of * {@link Map} that is an ideal candidate for this method's * {@code attributes} argument. @@ -1259,6 +1261,8 @@ public abstract class AnnotationUtils { * @since 4.2 * @see #synthesizeAnnotation(Annotation, AnnotatedElement) * @see #synthesizeAnnotation(Class) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) */ @SuppressWarnings("unchecked") public static A synthesizeAnnotation(Map attributes, @@ -1298,10 +1302,10 @@ public abstract class AnnotationUtils { } /** - * Synthesize the supplied array of {@code annotations} by - * creating a new array of the same size and type and populating it - * with {@linkplain #synthesizeAnnotation(Annotation) synthesized} - * versions of the annotations from the input array. + * Synthesize an array of annotations from the supplied array + * of {@code annotations} by creating a new array of the same size and + * type and populating it with {@linkplain #synthesizeAnnotation(Annotation) + * synthesized} versions of the annotations from the input array. * @param annotations the array of annotations to synthesize * @param annotatedElement the element that is annotated with the supplied * array of annotations; may be {@code null} if unknown @@ -1326,6 +1330,38 @@ public abstract class AnnotationUtils { return synthesized; } + /** + * Synthesize an array of annotations from the supplied array + * of {@code maps} of annotation attributes by creating a new array of + * {@code annotationType} with the same size and populating it with + * {@linkplain #synthesizeAnnotation(Map, Class, AnnotatedElement) + * synthesized} versions of the maps from the input array. + * @param maps the array of maps of annotation attributes to synthesize + * @param annotationType the type of annotations to synthesize; never + * {@code null} + * @return a new array of synthesized annotations, or {@code null} if + * the supplied array is {@code null} + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @since 4.2.1 + * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) + * @see #synthesizeAnnotationArray(Annotation[], AnnotatedElement) + */ + @SuppressWarnings("unchecked") + static A[] synthesizeAnnotationArray(Map[] maps, Class annotationType) { + Assert.notNull(annotationType, "annotationType must not be null"); + + if (maps == null) { + return null; + } + + A[] synthesized = (A[]) Array.newInstance(annotationType, maps.length); + for (int i = 0; i < maps.length; i++) { + synthesized[i] = synthesizeAnnotation(maps[i], annotationType, null); + } + return synthesized; + } + /** * Get a map of all attribute alias pairs, declared via {@code @AliasFor} * in the supplied annotation type. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java index 05ab3bd185..e8a2d9adc5 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java @@ -74,20 +74,24 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib /** - * Enrich and validate the supplied {@code attributes} map by ensuring + * Enrich and validate the supplied attributes map by ensuring * that it contains a non-null entry for each annotation attribute in * the specified {@code annotationType} and that the type of the entry * matches the return type for the corresponding annotation attribute. + *

If an entry is a map (presumably of annotation attributes), an + * attempt will be made to synthesize an annotation from it. Similarly, + * if an entry is an array of maps, an attempt will be made to synthesize + * an array of annotations from those maps. *

If an attribute is missing in the supplied map, it will be set - * either to value of its alias (if an alias value exists) or to the + * either to the value of its alias (if an alias exists) or to the * value of the attribute's default value (if defined), and otherwise * an {@link IllegalArgumentException} will be thrown. - * @see AliasFor */ + @SuppressWarnings("unchecked") private static Map enrichAndValidateAttributes( - Map original, Class annotationType) { + Map originalAttributes, Class annotationType) { - Map attributes = new HashMap(original); + Map attributes = new HashMap(originalAttributes); Map attributeAliasMap = getAttributeAliasMap(annotationType); for (Method attributeMethod : getAttributeMethods(annotationType)) { @@ -122,13 +126,41 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib attributes, attributeName, annotationType.getName())); } - // else, ensure correct type - Class returnType = attributeMethod.getReturnType(); - if (!ClassUtils.isAssignable(returnType, attributeValue.getClass())) { - throw new IllegalArgumentException(String.format( - "Attributes map [%s] returned a value of type [%s] for attribute [%s], " - + "but a value of type [%s] is required as defined by annotation type [%s].", attributes, - attributeValue.getClass().getName(), attributeName, returnType.getName(), annotationType.getName())); + // finally, ensure correct type + Class requiredReturnType = attributeMethod.getReturnType(); + Class actualReturnType = attributeValue.getClass(); + if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) { + boolean converted = false; + + // Nested map representing a single annotation? + if (Annotation.class.isAssignableFrom(requiredReturnType) + && Map.class.isAssignableFrom(actualReturnType)) { + + Class nestedAnnotationType = (Class) requiredReturnType; + Map map = (Map) attributeValue; + attributes.put(attributeName, synthesizeAnnotation(map, nestedAnnotationType, null)); + converted = true; + } + + // Nested array of maps representing an array of annotations? + else if (requiredReturnType.isArray() + && Annotation.class.isAssignableFrom(requiredReturnType.getComponentType()) + && actualReturnType.isArray() + && Map.class.isAssignableFrom(actualReturnType.getComponentType())) { + + Class nestedAnnotationType = (Class) requiredReturnType.getComponentType(); + Map[] maps = (Map[]) attributeValue; + attributes.put(attributeName, synthesizeAnnotationArray(maps, nestedAnnotationType)); + converted = true; + } + + if (!converted) { + throw new IllegalArgumentException(String.format( + "Attributes map [%s] returned a value of type [%s] for attribute [%s], " + + "but a value of type [%s] is required as defined by annotation type [%s].", + attributes, actualReturnType.getName(), attributeName, requiredReturnType.getName(), + annotationType.getName())); + } } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index 9ff5025581..b1095d13c9 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -883,6 +883,64 @@ public class AnnotationUtilsTests { assertEquals("value from synthesized component: ", "webController", synthesizedComponent.value()); } + @Test + @SuppressWarnings("unchecked") + public void synthesizeAnnotationFromMapWithNestedMap() throws Exception { + ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation(ComponentScanSingleFilter.class); + assertNotNull(componentScan); + assertEquals("value from ComponentScan: ", "*Foo", componentScan.value().pattern()); + + AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanSingleFilterClass.class, componentScan, + false, true); + assertNotNull(attributes); + assertEquals(ComponentScanSingleFilter.class, attributes.annotationType()); + + Map filterMap = (Map) attributes.get("value"); + assertNotNull(filterMap); + assertEquals("*Foo", filterMap.get("pattern")); + + // Modify nested map + filterMap.put("pattern", "newFoo"); + filterMap.put("enigma", 42); + + ComponentScanSingleFilter synthesizedComponentScan = synthesizeAnnotation(attributes, + ComponentScanSingleFilter.class, ComponentScanSingleFilterClass.class); + assertNotNull(synthesizedComponentScan); + + assertNotSame(componentScan, synthesizedComponentScan); + assertEquals("value from synthesized ComponentScan: ", "newFoo", synthesizedComponentScan.value().pattern()); + } + + @Test + @SuppressWarnings("unchecked") + public void synthesizeAnnotationFromMapWithNestedArrayOfMaps() throws Exception { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class); + assertNotNull(componentScan); + + AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanClass.class, componentScan, false, true); + assertNotNull(attributes); + assertEquals(ComponentScan.class, attributes.annotationType()); + + Map[] filters = (Map[]) attributes.get("excludeFilters"); + assertNotNull(filters); + + List patterns = stream(filters).map(m -> (String) m.get("pattern")).collect(toList()); + assertEquals(asList("*Foo", "*Bar"), patterns); + + // Modify nested maps + filters[0].put("pattern", "newFoo"); + filters[0].put("enigma", 42); + filters[1].put("pattern", "newBar"); + filters[1].put("enigma", 42); + + ComponentScan synthesizedComponentScan = synthesizeAnnotation(attributes, ComponentScan.class, ComponentScanClass.class); + assertNotNull(synthesizedComponentScan); + + assertNotSame(componentScan, synthesizedComponentScan); + patterns = stream(synthesizedComponentScan.excludeFilters()).map(Filter::pattern).collect(toList()); + assertEquals(asList("newFoo", "newBar"), patterns); + } + @Test public void synthesizeAnnotationFromDefaultsWithoutAttributeAliases() throws Exception { AnnotationWithDefaults annotationWithDefaults = synthesizeAnnotation(AnnotationWithDefaults.class); @@ -1711,6 +1769,18 @@ public class AnnotationUtilsTests { static class ComponentScanClass { } + /** + * Mock of {@code org.springframework.context.annotation.ComponentScan} + */ + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScanSingleFilter { + Filter value(); + } + + @ComponentScanSingleFilter(@Filter(pattern = "*Foo")) + static class ComponentScanSingleFilterClass { + } + @Retention(RetentionPolicy.RUNTIME) @interface AnnotationWithDefaults { String text() default "enigma";