From 31c547456a54d75f7b35eec0b684b81bb34555bf Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 12 Jun 2015 19:27:56 +0200 Subject: [PATCH] Introduce getAliasedString() in AnnotationAttributes Issue: SPR-11393 --- .../core/annotation/AnnotationAttributes.java | 123 +++++++++++------- .../annotation/AnnotationAttributesTests.java | 78 ++++++++++- 2 files changed, 153 insertions(+), 48 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java index 3990916dc6..1ab67ab6af 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -29,17 +29,21 @@ import org.springframework.util.StringUtils; /** * {@link LinkedHashMap} subclass representing annotation attribute - * key-value pairs as read by Spring's reflection- or ASM-based - * {@link org.springframework.core.type.AnnotationMetadata} implementations, - * {@link AnnotationUtils}, and {@link AnnotatedElementUtils}. + * key-value pairs as read by {@link AnnotationUtils}, + * {@link AnnotatedElementUtils}, and Spring's reflection- and ASM-based + * {@link org.springframework.core.type.AnnotationMetadata} implementations. * *

Provides 'pseudo-reification' to avoid noisy Map generics in the calling * code as well as convenience methods for looking up annotation attributes - * in a type-safe fashion. + * in a type-safe fashion, including support for attribute aliases configured + * via {@link AliasFor @AliasFor}. * * @author Chris Beams * @author Sam Brannen * @since 3.1.1 + * @see AnnotationUtils#getAnnotationAttributes + * @see AnnotatedElementUtils + * @see AliasFor */ @SuppressWarnings("serial") public class AnnotationAttributes extends LinkedHashMap { @@ -117,6 +121,34 @@ public class AnnotationAttributes extends LinkedHashMap { return getRequiredAttribute(attributeName, String.class); } + /** + * Get the value stored under the specified {@code attributeName} as a + * string, taking into account alias semantics defined via + * {@link AliasFor @AliasFor}. + *

If there is no value stored under the specified {@code attributeName} + * but the attribute has an alias declared via {@code @AliasFor}, the + * value of the alias will be returned. + * + * @param attributeName the name of the attribute to get; never + * {@code null} or empty + * @param annotationType the type of annotation represented by this + * {@code AnnotationAttributes} instance; never {@code null} + * @param annotationSource the source of the annotation represented by + * this {@code AnnotationAttributes} (e.g., the {@link AnnotatedElement}); + * or {@code null} if unknown + * @return the string value + * @throws IllegalArgumentException if the attribute and its alias do + * not exist or are not of type {@code String} + * @throws AnnotationConfigurationException if the attribute and its + * alias are both present with different non-empty values + * @since 4.2 + * @see ObjectUtils#isEmpty(Object) + */ + public String getAliasedString(String attributeName, Class annotationType, + Object annotationSource) { + return getRequiredAttributeWithAlias(attributeName, annotationType, annotationSource, String.class); + } + /** * Get the value stored under the specified {@code attributeName} as an * array of strings. @@ -157,7 +189,7 @@ public class AnnotationAttributes extends LinkedHashMap { */ public String[] getAliasedStringArray(String attributeName, Class annotationType, Object annotationSource) { - return getRequiredArrayWithAttributeAlias(attributeName, annotationType, annotationSource, String[].class); + return getRequiredAttributeWithAlias(attributeName, annotationType, annotationSource, String[].class); } /** @@ -255,7 +287,7 @@ public class AnnotationAttributes extends LinkedHashMap { */ public Class[] getAliasedClassArray(String attributeName, Class annotationType, Object annotationSource) { - return getRequiredArrayWithAttributeAlias(attributeName, annotationType, annotationSource, Class[].class); + return getRequiredAttributeWithAlias(attributeName, annotationType, annotationSource, Class[].class); } /** @@ -326,9 +358,39 @@ public class AnnotationAttributes extends LinkedHashMap { return (A[]) getRequiredAttribute(attributeName, array.getClass()); } + /** + * Get the value stored under the specified {@code attributeName}, + * ensuring that the value is of the {@code expectedType}. + *

If the {@code expectedType} is an array and the value stored + * under the specified {@code attributeName} is a single element of the + * component type of the expected array type, the single element will be + * wrapped in a single-element array of the appropriate type before + * returning it. + * @param attributeName the name of the attribute to get; never + * {@code null} or empty + * @param expectedType the expected type; never {@code null} + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + @SuppressWarnings("unchecked") + private T getRequiredAttribute(String attributeName, Class expectedType) { + Assert.hasText(attributeName, "attributeName must not be null or empty"); + Object value = get(attributeName); + assertAttributePresence(attributeName, value); + if (!expectedType.isInstance(value) && expectedType.isArray() + && expectedType.getComponentType().isInstance(value)) { + Object array = Array.newInstance(expectedType.getComponentType(), 1); + Array.set(array, 0, value); + value = array; + } + assertAttributeType(attributeName, value, expectedType); + return (T) value; + } + /** * Get the value stored under the specified {@code attributeName} as an - * array of the {@code expectedType}, taking into account alias semantics + * object of the {@code expectedType}, taking into account alias semantics * defined via {@link AliasFor @AliasFor}. *

If there is no value stored under the specified {@code attributeName} * but the attribute has an alias declared via {@code @AliasFor}, the @@ -341,34 +403,33 @@ public class AnnotationAttributes extends LinkedHashMap { * @param annotationSource the source of the annotation represented by * this {@code AnnotationAttributes} (e.g., the {@link AnnotatedElement}); * or {@code null} if unknown - * @param expectedType the expected array type; never {@code null} - * @return the array of values + * @param expectedType the expected type; never {@code null} + * @return the value * @throws IllegalArgumentException if the attribute and its alias do - * not exist or are not of the {@code expectedType}, or if the - * {@code expectedType} is not an array + * not exist or are not of the {@code expectedType} * @throws AnnotationConfigurationException if the attribute and its * alias are both present with different non-empty values * @since 4.2 + * @see ObjectUtils#isEmpty(Object) */ - private T getRequiredArrayWithAttributeAlias(String attributeName, Class annotationType, + private T getRequiredAttributeWithAlias(String attributeName, Class annotationType, Object annotationSource, Class expectedType) { Assert.hasText(attributeName, "attributeName must not be null or empty"); Assert.notNull(annotationType, "annotationType must not be null"); Assert.notNull(expectedType, "expectedType must not be null"); - Assert.isTrue(expectedType.isArray(), "expectedType must be an array"); T attributeValue = getAttribute(attributeName, expectedType); String aliasName = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName); T aliasValue = getAttribute(aliasName, expectedType); - boolean attributeDeclared = !ObjectUtils.isEmpty((Object[]) attributeValue); - boolean aliasDeclared = !ObjectUtils.isEmpty((Object[]) aliasValue); + boolean attributeDeclared = !ObjectUtils.isEmpty(attributeValue); + boolean aliasDeclared = !ObjectUtils.isEmpty(aliasValue); if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) && attributeDeclared && aliasDeclared) { String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString()); String msg = String.format("In annotation [%s] declared on [%s], " + "attribute [%s] and its alias [%s] are present with values of [%s] and [%s], " - + "but only one is permitted.", this.displayName, elementName, attributeName, aliasName, + + "but only one is permitted.", annotationType.getName(), elementName, attributeName, aliasName, ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)); throw new AnnotationConfigurationException(msg); } @@ -402,36 +463,6 @@ public class AnnotationAttributes extends LinkedHashMap { return (T) value; } - /** - * Get the value stored under the specified {@code attributeName}, - * ensuring that the value is of the {@code expectedType}. - *

If the {@code expectedType} is an array and the value stored - * under the specified {@code attributeName} is a single element of the - * component type of the expected array type, the single element will be - * wrapped in a single-element array of the appropriate type before - * returning it. - * @param attributeName the name of the attribute to get; never - * {@code null} or empty - * @param expectedType the expected type; never {@code null} - * @return the value - * @throws IllegalArgumentException if the attribute does not exist or - * if it is not of the expected type - */ - @SuppressWarnings("unchecked") - private T getRequiredAttribute(String attributeName, Class expectedType) { - Assert.hasText(attributeName, "attributeName must not be null or empty"); - Object value = get(attributeName); - assertAttributePresence(attributeName, value); - if (!expectedType.isInstance(value) && expectedType.isArray() - && expectedType.getComponentType().isInstance(value)) { - Object array = Array.newInstance(expectedType.getComponentType(), 1); - Array.set(array, 0, value); - value = array; - } - assertAttributeType(attributeName, value, expectedType); - return (T) value; - } - private void assertAttributePresence(String attributeName, Object attributeValue) { if (attributeValue == null) { throw new IllegalArgumentException(String.format( diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java index 4de1c2a311..15eaec6b30 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java @@ -144,6 +144,63 @@ public class AnnotationAttributesTests { attributes.getEnum("color"); } + @Test + public void getAliasedString() { + attributes.clear(); + attributes.put("name", "metaverse"); + assertEquals("metaverse", getAliasedString("name")); + assertEquals("metaverse", getAliasedString("value")); + + attributes.clear(); + attributes.put("value", "metaverse"); + assertEquals("metaverse", getAliasedString("name")); + assertEquals("metaverse", getAliasedString("value")); + + attributes.clear(); + attributes.put("name", "metaverse"); + attributes.put("value", "metaverse"); + assertEquals("metaverse", getAliasedString("name")); + assertEquals("metaverse", getAliasedString("value")); + } + + @Test + public void getAliasedStringFromSynthesizedAnnotationAttributes() { + Scope scope = ScopedComponent.class.getAnnotation(Scope.class); + AnnotationAttributes scopeAttributes = AnnotationUtils.getAnnotationAttributes(ScopedComponent.class, scope); + + assertEquals("custom", getAliasedString(scopeAttributes, "name")); + assertEquals("custom", getAliasedString(scopeAttributes, "value")); + } + + @Test + public void getAliasedStringWithMissingAliasedAttributes() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage(equalTo("Neither attribute 'name' nor its alias 'value' was found in attributes for annotation [unknown]")); + getAliasedString("name"); + } + + @Test + public void getAliasedStringWithDifferentAliasedValues() { + attributes.put("name", "request"); + attributes.put("value", "session"); + + exception.expect(AnnotationConfigurationException.class); + exception.expectMessage(containsString("In annotation [" + Scope.class.getName() + "]")); + exception.expectMessage(containsString("attribute [name] and its alias [value]")); + exception.expectMessage(containsString("[request] and [session]")); + exception.expectMessage(containsString("but only one is permitted")); + + getAliasedString("name"); + } + + private String getAliasedString(String attributeName) { + return getAliasedString(this.attributes, attributeName); + } + + private String getAliasedString(AnnotationAttributes attrs, String attributeName) { + return attrs.getAliasedString(attributeName, Scope.class, null); + } + @Test public void getAliasedStringArray() { final String[] INPUT = new String[] { "test.xml" }; @@ -197,7 +254,7 @@ public class AnnotationAttributesTests { attributes.put("value", new String[] { "2.xml" }); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(containsString("In annotation [unknown]")); + exception.expectMessage(containsString("In annotation [" + ContextConfig.class.getName() + "]")); exception.expectMessage(containsString("attribute [locations] and its alias [value]")); exception.expectMessage(containsString("[{1.xml}] and [{2.xml}]")); exception.expectMessage(containsString("but only one is permitted")); @@ -262,7 +319,7 @@ public class AnnotationAttributesTests { attributes.put("value", new Class[] { Number.class }); exception.expect(AnnotationConfigurationException.class); - exception.expectMessage(containsString("In annotation [unknown]")); + exception.expectMessage(containsString("In annotation [" + Filter.class.getName() + "]")); exception.expectMessage(containsString("attribute [classes] and its alias [value]")); exception.expectMessage(containsString("[{class java.lang.String}] and [{class java.lang.Number}]")); exception.expectMessage(containsString("but only one is permitted")); @@ -308,4 +365,21 @@ public class AnnotationAttributesTests { String locations() default ""; } + /** + * Mock of {@code org.springframework.context.annotation.Scope}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface Scope { + + @AliasFor(attribute = "name") + String value() default "singleton"; + + @AliasFor(attribute = "value") + String name() default "singleton"; + } + + @Scope(name = "custom") + static class ScopedComponent { + } + }