From 0ac0e2ce2067f40362ebbc4d4deb5eee251b149a Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 26 May 2015 16:43:50 +0200 Subject: [PATCH] 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 --- .../core/annotation/AnnotationAttributes.java | 139 ++++++++++++++++-- .../annotation/AnnotationAttributesTests.java | 32 +++- 2 files changed, 150 insertions(+), 21 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 dd98bb794e..426648be87 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 @@ -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 + * key-value 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 { * 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 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 { } /** - * 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 key-value pairs. + * @param map original source of annotation attribute key-value + * pairs * @see #fromMap(Map) */ public AnnotationAttributes(Map map) { @@ -91,8 +93,8 @@ public class AnnotationAttributes extends LinkedHashMap { } /** - * 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 { 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. + *

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 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 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 Class getClass(String attributeName) { return doGet(attributeName, Class.class); } + /** + * Get the value stored under the specified {@code attributeName} as an + * array of classes. + *

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}. + *

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}. + *

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 doGet(String attributeName, Class expectedType) { Assert.hasText(attributeName, "attributeName must not be null or empty"); @@ -149,9 +257,9 @@ public class AnnotationAttributes extends LinkedHashMap { } 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 { * 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 { * 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 key-value pairs */ public static AnnotationAttributes fromMap(Map map) { if (map == null) { 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 16d8f9a5f6..a547599748 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 @@ -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 { assertThat(attributes.getBoolean("bool1"), equalTo(true)); assertThat(attributes.getBoolean("bool2"), equalTo(false)); assertThat(attributes.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.getNumber("number"), equalTo(42)); assertThat(attributes.getAnnotation("anno").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);