Skip to content

Commit f17173f

Browse files
committed
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
1 parent 8289036 commit f17173f

File tree

3 files changed

+155
-17
lines changed

3 files changed

+155
-17
lines changed

spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,7 +1242,9 @@ public static <A extends Annotation> A synthesizeAnnotation(A annotation, Annota
12421242
* that are annotated with {@link AliasFor @AliasFor}.
12431243
* <p>The supplied map must contain a key-value pair for every attribute
12441244
* defined in the supplied {@code annotationType} that is not aliased or
1245-
* does not have a default value.
1245+
* does not have a default value. Nested maps and nested arrays of maps
1246+
* will be recursively synthesized into nested annotations or nested
1247+
* arrays of annotations, respectively.
12461248
* <p>Note that {@link AnnotationAttributes} is a specialized type of
12471249
* {@link Map} that is an ideal candidate for this method's
12481250
* {@code attributes} argument.
@@ -1259,6 +1261,8 @@ public static <A extends Annotation> A synthesizeAnnotation(A annotation, Annota
12591261
* @since 4.2
12601262
* @see #synthesizeAnnotation(Annotation, AnnotatedElement)
12611263
* @see #synthesizeAnnotation(Class)
1264+
* @see #getAnnotationAttributes(AnnotatedElement, Annotation)
1265+
* @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean)
12621266
*/
12631267
@SuppressWarnings("unchecked")
12641268
public static <A extends Annotation> A synthesizeAnnotation(Map<String, Object> attributes,
@@ -1298,10 +1302,10 @@ public static <A extends Annotation> A synthesizeAnnotation(Class<A> annotationT
12981302
}
12991303

13001304
/**
1301-
* <em>Synthesize</em> the supplied array of {@code annotations} by
1302-
* creating a new array of the same size and type and populating it
1303-
* with {@linkplain #synthesizeAnnotation(Annotation) synthesized}
1304-
* versions of the annotations from the input array.
1305+
* <em>Synthesize</em> an array of annotations from the supplied array
1306+
* of {@code annotations} by creating a new array of the same size and
1307+
* type and populating it with {@linkplain #synthesizeAnnotation(Annotation)
1308+
* synthesized} versions of the annotations from the input array.
13051309
* @param annotations the array of annotations to synthesize
13061310
* @param annotatedElement the element that is annotated with the supplied
13071311
* array of annotations; may be {@code null} if unknown
@@ -1326,6 +1330,38 @@ public static Annotation[] synthesizeAnnotationArray(Annotation[] annotations, A
13261330
return synthesized;
13271331
}
13281332

1333+
/**
1334+
* <em>Synthesize</em> an array of annotations from the supplied array
1335+
* of {@code maps} of annotation attributes by creating a new array of
1336+
* {@code annotationType} with the same size and populating it with
1337+
* {@linkplain #synthesizeAnnotation(Map, Class, AnnotatedElement)
1338+
* synthesized} versions of the maps from the input array.
1339+
* @param maps the array of maps of annotation attributes to synthesize
1340+
* @param annotationType the type of annotations to synthesize; never
1341+
* {@code null}
1342+
* @return a new array of synthesized annotations, or {@code null} if
1343+
* the supplied array is {@code null}
1344+
* @throws AnnotationConfigurationException if invalid configuration of
1345+
* {@code @AliasFor} is detected
1346+
* @since 4.2.1
1347+
* @see #synthesizeAnnotation(Map, Class, AnnotatedElement)
1348+
* @see #synthesizeAnnotationArray(Annotation[], AnnotatedElement)
1349+
*/
1350+
@SuppressWarnings("unchecked")
1351+
static <A extends Annotation> A[] synthesizeAnnotationArray(Map<String, Object>[] maps, Class<A> annotationType) {
1352+
Assert.notNull(annotationType, "annotationType must not be null");
1353+
1354+
if (maps == null) {
1355+
return null;
1356+
}
1357+
1358+
A[] synthesized = (A[]) Array.newInstance(annotationType, maps.length);
1359+
for (int i = 0; i < maps.length; i++) {
1360+
synthesized[i] = synthesizeAnnotation(maps[i], annotationType, null);
1361+
}
1362+
return synthesized;
1363+
}
1364+
13291365
/**
13301366
* Get a map of all attribute alias pairs, declared via {@code @AliasFor}
13311367
* in the supplied annotation type.

spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,24 @@ protected Object getRawAttributeValue(String attributeName) {
7474

7575

7676
/**
77-
* Enrich and validate the supplied {@code attributes} map by ensuring
77+
* Enrich and validate the supplied <em>attributes</em> map by ensuring
7878
* that it contains a non-null entry for each annotation attribute in
7979
* the specified {@code annotationType} and that the type of the entry
8080
* matches the return type for the corresponding annotation attribute.
81+
* <p>If an entry is a map (presumably of annotation attributes), an
82+
* attempt will be made to synthesize an annotation from it. Similarly,
83+
* if an entry is an array of maps, an attempt will be made to synthesize
84+
* an array of annotations from those maps.
8185
* <p>If an attribute is missing in the supplied map, it will be set
82-
* either to value of its alias (if an alias value exists) or to the
86+
* either to the value of its alias (if an alias exists) or to the
8387
* value of the attribute's default value (if defined), and otherwise
8488
* an {@link IllegalArgumentException} will be thrown.
85-
* @see AliasFor
8689
*/
90+
@SuppressWarnings("unchecked")
8791
private static Map<String, Object> enrichAndValidateAttributes(
88-
Map<String, Object> original, Class<? extends Annotation> annotationType) {
92+
Map<String, Object> originalAttributes, Class<? extends Annotation> annotationType) {
8993

90-
Map<String, Object> attributes = new HashMap<String, Object>(original);
94+
Map<String, Object> attributes = new HashMap<String, Object>(originalAttributes);
9195
Map<String, String> attributeAliasMap = getAttributeAliasMap(annotationType);
9296

9397
for (Method attributeMethod : getAttributeMethods(annotationType)) {
@@ -122,13 +126,41 @@ private static Map<String, Object> enrichAndValidateAttributes(
122126
attributes, attributeName, annotationType.getName()));
123127
}
124128

125-
// else, ensure correct type
126-
Class<?> returnType = attributeMethod.getReturnType();
127-
if (!ClassUtils.isAssignable(returnType, attributeValue.getClass())) {
128-
throw new IllegalArgumentException(String.format(
129-
"Attributes map [%s] returned a value of type [%s] for attribute [%s], "
130-
+ "but a value of type [%s] is required as defined by annotation type [%s].", attributes,
131-
attributeValue.getClass().getName(), attributeName, returnType.getName(), annotationType.getName()));
129+
// finally, ensure correct type
130+
Class<?> requiredReturnType = attributeMethod.getReturnType();
131+
Class<? extends Object> actualReturnType = attributeValue.getClass();
132+
if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) {
133+
boolean converted = false;
134+
135+
// Nested map representing a single annotation?
136+
if (Annotation.class.isAssignableFrom(requiredReturnType)
137+
&& Map.class.isAssignableFrom(actualReturnType)) {
138+
139+
Class<? extends Annotation> nestedAnnotationType = (Class<? extends Annotation>) requiredReturnType;
140+
Map<String, Object> map = (Map<String, Object>) attributeValue;
141+
attributes.put(attributeName, synthesizeAnnotation(map, nestedAnnotationType, null));
142+
converted = true;
143+
}
144+
145+
// Nested array of maps representing an array of annotations?
146+
else if (requiredReturnType.isArray()
147+
&& Annotation.class.isAssignableFrom(requiredReturnType.getComponentType())
148+
&& actualReturnType.isArray()
149+
&& Map.class.isAssignableFrom(actualReturnType.getComponentType())) {
150+
151+
Class<? extends Annotation> nestedAnnotationType = (Class<? extends Annotation>) requiredReturnType.getComponentType();
152+
Map<String, Object>[] maps = (Map<String, Object>[]) attributeValue;
153+
attributes.put(attributeName, synthesizeAnnotationArray(maps, nestedAnnotationType));
154+
converted = true;
155+
}
156+
157+
if (!converted) {
158+
throw new IllegalArgumentException(String.format(
159+
"Attributes map [%s] returned a value of type [%s] for attribute [%s], "
160+
+ "but a value of type [%s] is required as defined by annotation type [%s].",
161+
attributes, actualReturnType.getName(), attributeName, requiredReturnType.getName(),
162+
annotationType.getName()));
163+
}
132164
}
133165
}
134166

spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,64 @@ public void synthesizeAnnotationFromMapWithoutAttributeAliases() throws Exceptio
883883
assertEquals("value from synthesized component: ", "webController", synthesizedComponent.value());
884884
}
885885

886+
@Test
887+
@SuppressWarnings("unchecked")
888+
public void synthesizeAnnotationFromMapWithNestedMap() throws Exception {
889+
ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation(ComponentScanSingleFilter.class);
890+
assertNotNull(componentScan);
891+
assertEquals("value from ComponentScan: ", "*Foo", componentScan.value().pattern());
892+
893+
AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanSingleFilterClass.class, componentScan,
894+
false, true);
895+
assertNotNull(attributes);
896+
assertEquals(ComponentScanSingleFilter.class, attributes.annotationType());
897+
898+
Map<String, Object> filterMap = (Map<String, Object>) attributes.get("value");
899+
assertNotNull(filterMap);
900+
assertEquals("*Foo", filterMap.get("pattern"));
901+
902+
// Modify nested map
903+
filterMap.put("pattern", "newFoo");
904+
filterMap.put("enigma", 42);
905+
906+
ComponentScanSingleFilter synthesizedComponentScan = synthesizeAnnotation(attributes,
907+
ComponentScanSingleFilter.class, ComponentScanSingleFilterClass.class);
908+
assertNotNull(synthesizedComponentScan);
909+
910+
assertNotSame(componentScan, synthesizedComponentScan);
911+
assertEquals("value from synthesized ComponentScan: ", "newFoo", synthesizedComponentScan.value().pattern());
912+
}
913+
914+
@Test
915+
@SuppressWarnings("unchecked")
916+
public void synthesizeAnnotationFromMapWithNestedArrayOfMaps() throws Exception {
917+
ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class);
918+
assertNotNull(componentScan);
919+
920+
AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanClass.class, componentScan, false, true);
921+
assertNotNull(attributes);
922+
assertEquals(ComponentScan.class, attributes.annotationType());
923+
924+
Map<String, Object>[] filters = (Map[]) attributes.get("excludeFilters");
925+
assertNotNull(filters);
926+
927+
List<String> patterns = stream(filters).map(m -> (String) m.get("pattern")).collect(toList());
928+
assertEquals(asList("*Foo", "*Bar"), patterns);
929+
930+
// Modify nested maps
931+
filters[0].put("pattern", "newFoo");
932+
filters[0].put("enigma", 42);
933+
filters[1].put("pattern", "newBar");
934+
filters[1].put("enigma", 42);
935+
936+
ComponentScan synthesizedComponentScan = synthesizeAnnotation(attributes, ComponentScan.class, ComponentScanClass.class);
937+
assertNotNull(synthesizedComponentScan);
938+
939+
assertNotSame(componentScan, synthesizedComponentScan);
940+
patterns = stream(synthesizedComponentScan.excludeFilters()).map(Filter::pattern).collect(toList());
941+
assertEquals(asList("newFoo", "newBar"), patterns);
942+
}
943+
886944
@Test
887945
public void synthesizeAnnotationFromDefaultsWithoutAttributeAliases() throws Exception {
888946
AnnotationWithDefaults annotationWithDefaults = synthesizeAnnotation(AnnotationWithDefaults.class);
@@ -1711,6 +1769,18 @@ static class AliasedComposedContextConfigNotMetaPresentClass {
17111769
static class ComponentScanClass {
17121770
}
17131771

1772+
/**
1773+
* Mock of {@code org.springframework.context.annotation.ComponentScan}
1774+
*/
1775+
@Retention(RetentionPolicy.RUNTIME)
1776+
@interface ComponentScanSingleFilter {
1777+
Filter value();
1778+
}
1779+
1780+
@ComponentScanSingleFilter(@Filter(pattern = "*Foo"))
1781+
static class ComponentScanSingleFilterClass {
1782+
}
1783+
17141784
@Retention(RetentionPolicy.RUNTIME)
17151785
@interface AnnotationWithDefaults {
17161786
String text() default "enigma";

0 commit comments

Comments
 (0)