Introduce getAliasedString() in AnnotationAttributes
Issue: SPR-11393
This commit is contained in:
@@ -29,17 +29,21 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link LinkedHashMap} subclass representing annotation attribute
|
||||
* <em>key-value</em> pairs as read by Spring's reflection- or ASM-based
|
||||
* {@link org.springframework.core.type.AnnotationMetadata} implementations,
|
||||
* {@link AnnotationUtils}, and {@link AnnotatedElementUtils}.
|
||||
* <em>key-value</em> pairs as read by {@link AnnotationUtils},
|
||||
* {@link AnnotatedElementUtils}, and Spring's reflection- and ASM-based
|
||||
* {@link org.springframework.core.type.AnnotationMetadata} implementations.
|
||||
*
|
||||
* <p>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<String, Object> {
|
||||
@@ -117,6 +121,34 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
|
||||
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}.
|
||||
* <p>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<? extends Annotation> 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<String, Object> {
|
||||
*/
|
||||
public String[] getAliasedStringArray(String attributeName, Class<? extends Annotation> 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<String, Object> {
|
||||
*/
|
||||
public Class<?>[] getAliasedClassArray(String attributeName, Class<? extends Annotation> 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<String, Object> {
|
||||
return (A[]) getRequiredAttribute(attributeName, array.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value stored under the specified {@code attributeName},
|
||||
* ensuring that the value is of the {@code expectedType}.
|
||||
* <p>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> T getRequiredAttribute(String attributeName, Class<T> 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}.
|
||||
* <p>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<String, Object> {
|
||||
* @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> T getRequiredArrayWithAttributeAlias(String attributeName, Class<? extends Annotation> annotationType,
|
||||
private <T> T getRequiredAttributeWithAlias(String attributeName, Class<? extends Annotation> annotationType,
|
||||
Object annotationSource, Class<T> 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<String, Object> {
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value stored under the specified {@code attributeName},
|
||||
* ensuring that the value is of the {@code expectedType}.
|
||||
* <p>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> T getRequiredAttribute(String attributeName, Class<T> 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(
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user