Browse Source

Document public API in AnnotationAttributes

AnnotationAttributes has existed for several years, but none of the
"get" methods that make up its public API are documented. In many
cases, the behavior can be inferred from the name of the method, but
for some methods there are "hidden gems" and unexpected behavior
lurking behind the scenes.

This commit addresses this issue by documenting all public methods. In
addition, the hidden support for converting single elements into
single-element arrays has also been documented and tested.

Issue: SPR-13072
pull/808/head
Sam Brannen 10 years ago
parent
commit
0ac0e2ce20
  1. 139
      spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java
  2. 32
      spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java

139
spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java

@ -26,8 +26,8 @@ import org.springframework.util.Assert; @@ -26,8 +26,8 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link LinkedHashMap} subclass representing annotation attribute key/value
* pairs as read by Spring's reflection- or ASM-based
* {@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}.
*
@ -59,17 +59,18 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -59,17 +59,18 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
* Create a new, empty {@link AnnotationAttributes} instance for the
* specified {@code annotationType}.
* @param annotationType the type of annotation represented by this
* {@code AnnotationAttributes} instance
* {@code AnnotationAttributes} instance; never {@code null}
* @since 4.2
*/
public AnnotationAttributes(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "annotationType must not be null");
this.annotationType = annotationType;
this.displayName = (annotationType() != null ? annotationType.getName() : "unknown");
this.displayName = annotationType.getName();
}
/**
* Create a new, empty {@link AnnotationAttributes} instance with the given initial
* capacity to optimize performance.
* Create a new, empty {@link AnnotationAttributes} instance with the
* given initial capacity to optimize performance.
* @param initialCapacity initial size of the underlying map
*/
public AnnotationAttributes(int initialCapacity) {
@ -79,9 +80,10 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -79,9 +80,10 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
}
/**
* Create a new {@link AnnotationAttributes} instance, wrapping the provided map
* and all its key/value pairs.
* @param map original source of annotation attribute key/value pairs to wrap
* Create a new {@link AnnotationAttributes} instance, wrapping the
* provided map and all its <em>key-value</em> pairs.
* @param map original source of annotation attribute <em>key-value</em>
* pairs
* @see #fromMap(Map)
*/
public AnnotationAttributes(Map<String, Object> map) {
@ -91,8 +93,8 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -91,8 +93,8 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
}
/**
* Get the type of annotation represented by this {@code AnnotationAttributes}
* instance.
* Get the type of annotation represented by this
* {@code AnnotationAttributes} instance.
* @return the annotation type, or {@code null} if unknown
* @since 4.2
*/
@ -100,45 +102,151 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -100,45 +102,151 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
return this.annotationType;
}
/**
* Get the value stored under the specified {@code attributeName} as a
* string.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the value
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
public String getString(String attributeName) {
return doGet(attributeName, String.class);
}
/**
* Get the value stored under the specified {@code attributeName} as an
* array of strings.
* <p>If the value stored under the specified {@code attributeName} is
* a string, it will be wrapped in a single-element array before
* returning it.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the value
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
public String[] getStringArray(String attributeName) {
return doGet(attributeName, String[].class);
}
/**
* Get the value stored under the specified {@code attributeName} as a
* boolean.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the value
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
public boolean getBoolean(String attributeName) {
return doGet(attributeName, Boolean.class);
}
/**
* Get the value stored under the specified {@code attributeName} as a
* number.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the value
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
@SuppressWarnings("unchecked")
public <N extends Number> N getNumber(String attributeName) {
return (N) doGet(attributeName, Number.class);
}
/**
* Get the value stored under the specified {@code attributeName} as an
* enum.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the value
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
@SuppressWarnings("unchecked")
public <E extends Enum<?>> E getEnum(String attributeName) {
return (E) doGet(attributeName, Enum.class);
}
/**
* Get the value stored under the specified {@code attributeName} as a
* class.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the value
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
@SuppressWarnings("unchecked")
public <T> Class<? extends T> getClass(String attributeName) {
return doGet(attributeName, Class.class);
}
/**
* Get the value stored under the specified {@code attributeName} as an
* array of classes.
* <p>If the value stored under the specified {@code attributeName} is
* a class, it will be wrapped in a single-element array before
* returning it.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the value
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
public Class<?>[] getClassArray(String attributeName) {
return doGet(attributeName, Class[].class);
}
/**
* Get the {@link AnnotationAttributes} stored under the specified
* {@code attributeName}.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the {@code AnnotationAttributes}
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
public AnnotationAttributes getAnnotation(String attributeName) {
return doGet(attributeName, AnnotationAttributes.class);
}
/**
* Get the array of {@link AnnotationAttributes} stored under the specified
* {@code attributeName}.
* <p>If the value stored under the specified {@code attributeName} is
* an instance of {@code AnnotationAttributes}, it will be wrapped in
* a single-element array before returning it.
* @param attributeName the name of the attribute to get; never
* {@code null} or empty
* @return the array of {@code AnnotationAttributes}
* @throws IllegalArgumentException if the attribute does not exist or
* if it is not of the expected type
*/
public AnnotationAttributes[] getAnnotationArray(String attributeName) {
return doGet(attributeName, AnnotationAttributes[].class);
}
/**
* 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
* @since 4.2
*/
@SuppressWarnings("unchecked")
private <T> T doGet(String attributeName, Class<T> expectedType) {
Assert.hasText(attributeName, "attributeName must not be null or empty");
@ -149,9 +257,9 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -149,9 +257,9 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
}
if (!expectedType.isInstance(value)) {
if (expectedType.isArray() && expectedType.getComponentType().isInstance(value)) {
Object arrayValue = Array.newInstance(expectedType.getComponentType(), 1);
Array.set(arrayValue, 0, value);
value = arrayValue;
Object array = Array.newInstance(expectedType.getComponentType(), 1);
Array.set(array, 0, value);
value = array;
}
else {
throw new IllegalArgumentException(String.format(
@ -171,6 +279,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -171,6 +279,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
* value was previously stored in this map
* @see #get
* @see #put
* @since 4.2
*/
@Override
public Object putIfAbsent(String key, Object value) {
@ -213,7 +322,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -213,7 +322,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
* will be cast and returned immediately without creating a new instance.
* Otherwise a new instance will be created by passing the supplied map
* to the {@link #AnnotationAttributes(Map)} constructor.
* @param map original source of annotation attribute key/value pairs
* @param map original source of annotation attribute <em>key-value</em> pairs
*/
public static AnnotationAttributes fromMap(Map<String, Object> map) {
if (map == null) {

32
spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java

@ -44,18 +44,18 @@ public class AnnotationAttributesTests { @@ -44,18 +44,18 @@ public class AnnotationAttributesTests {
@Test
public void typeSafeAttributeAccess() {
AnnotationAttributes nestedAttributes = new AnnotationAttributes();
nestedAttributes.put("value", 10);
nestedAttributes.put("name", "algernon");
attributes.put("name", "dave");
attributes.put("names", new String[] { "dave", "frank", "hal" });
attributes.put("bool1", true);
attributes.put("bool2", false);
attributes.put("color", Color.RED);
attributes.put("clazz", Integer.class);
attributes.put("class", Integer.class);
attributes.put("classes", new Class<?>[] { Number.class, Short.class, Integer.class });
attributes.put("number", 42);
attributes.put("numbers", new int[] { 42, 43 });
AnnotationAttributes nestedAttributes = new AnnotationAttributes();
nestedAttributes.put("value", 10);
nestedAttributes.put("name", "algernon");
attributes.put("anno", nestedAttributes);
attributes.put("annoArray", new AnnotationAttributes[] { nestedAttributes });
@ -64,13 +64,33 @@ public class AnnotationAttributesTests { @@ -64,13 +64,33 @@ public class AnnotationAttributesTests {
assertThat(attributes.getBoolean("bool1"), equalTo(true));
assertThat(attributes.getBoolean("bool2"), equalTo(false));
assertThat(attributes.<Color>getEnum("color"), equalTo(Color.RED));
assertTrue(attributes.getClass("clazz").equals(Integer.class));
assertTrue(attributes.getClass("class").equals(Integer.class));
assertThat(attributes.getClassArray("classes"), equalTo(new Class[] { Number.class, Short.class, Integer.class }));
assertThat(attributes.<Integer>getNumber("number"), equalTo(42));
assertThat(attributes.getAnnotation("anno").<Integer>getNumber("value"), equalTo(10));
assertThat(attributes.getAnnotationArray("annoArray")[0].getString("name"), equalTo("algernon"));
}
@Test
public void singleElementToSingleElementArrayConversionSupport() {
AnnotationAttributes nestedAttributes = new AnnotationAttributes();
nestedAttributes.put("name", "Dilbert");
// Store single elements
attributes.put("names", "Dogbert");
attributes.put("classes", Number.class);
attributes.put("nestedAttributes", nestedAttributes);
// Get back arrays of single elements
assertThat(attributes.getStringArray("names"), equalTo(new String[] { "Dogbert" }));
assertThat(attributes.getClassArray("classes"), equalTo(new Class[] { Number.class }));
AnnotationAttributes[] array = attributes.getAnnotationArray("nestedAttributes");
assertNotNull(array);
assertTrue(array.getClass().isArray());
assertThat(array.length, is(1));
assertThat(array[0].getString("name"), equalTo("Dilbert"));
}
@Test
public void getEnumWithNullAttributeName() {
exception.expect(IllegalArgumentException.class);

Loading…
Cancel
Save