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
This commit is contained in:
Sam Brannen 2015-08-10 14:55:54 +02:00
parent 8289036165
commit f17173f6d5
3 changed files with 155 additions and 17 deletions

View File

@ -1242,7 +1242,9 @@ public abstract class AnnotationUtils {
* that are annotated with {@link AliasFor @AliasFor}.
* <p>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.
* <p>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 extends Annotation> A synthesizeAnnotation(Map<String, Object> attributes,
@ -1298,10 +1302,10 @@ public abstract class AnnotationUtils {
}
/**
* <em>Synthesize</em> 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.
* <em>Synthesize</em> 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;
}
/**
* <em>Synthesize</em> 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 extends Annotation> A[] synthesizeAnnotationArray(Map<String, Object>[] maps, Class<A> 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.

View File

@ -74,20 +74,24 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib
/**
* Enrich and validate the supplied {@code attributes} map by ensuring
* Enrich and validate the supplied <em>attributes</em> 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.
* <p>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.
* <p>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<String, Object> enrichAndValidateAttributes(
Map<String, Object> original, Class<? extends Annotation> annotationType) {
Map<String, Object> originalAttributes, Class<? extends Annotation> annotationType) {
Map<String, Object> attributes = new HashMap<String, Object>(original);
Map<String, Object> attributes = new HashMap<String, Object>(originalAttributes);
Map<String, String> 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<? extends Object> 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<? extends Annotation> nestedAnnotationType = (Class<? extends Annotation>) requiredReturnType;
Map<String, Object> map = (Map<String, Object>) 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<? extends Annotation> nestedAnnotationType = (Class<? extends Annotation>) requiredReturnType.getComponentType();
Map<String, Object>[] maps = (Map<String, Object>[]) 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()));
}
}
}

View File

@ -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<String, Object> filterMap = (Map<String, Object>) 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<String, Object>[] filters = (Map[]) attributes.get("excludeFilters");
assertNotNull(filters);
List<String> 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";