Browse Source

Merge from sbrannen/SPR-13345

* SPR-13345:
  Support implicit attribute aliases with @AliasFor
pull/867/head
Sam Brannen 10 years ago
parent
commit
3eacb837c2
  1. 49
      spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java
  2. 56
      spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java
  3. 17
      spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java
  4. 53
      spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java
  5. 600
      spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java
  6. 24
      spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java
  7. 68
      spring-core/src/test/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractorTestCase.java
  8. 122
      spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java
  9. 219
      spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java
  10. 379
      spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java
  11. 34
      spring-core/src/test/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractorTests.java
  12. 127
      spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java

49
spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java

@ -19,6 +19,7 @@ package org.springframework.core.annotation; @@ -19,6 +19,7 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import org.springframework.util.Assert;
@ -44,7 +45,7 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno @@ -44,7 +45,7 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
private final S source;
private final Map<String, String> attributeAliasMap;
private final Map<String, List<String>> attributeAliasMap;
/**
@ -83,29 +84,33 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno @@ -83,29 +84,33 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
@Override
public final Object getAttributeValue(Method attributeMethod) {
String attributeName = attributeMethod.getName();
final String attributeName = attributeMethod.getName();
Object attributeValue = getRawAttributeValue(attributeMethod);
String aliasName = this.attributeAliasMap.get(attributeName);
if (aliasName != null) {
Object aliasValue = getRawAttributeValue(aliasName);
Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
!ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
String elementName = (getAnnotatedElement() != null ? getAnnotatedElement().toString() : "unknown element");
throw new AnnotationConfigurationException(String.format(
"In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " +
"alias '%s' are present with values of [%s] and [%s], but only one is permitted.",
getAnnotationType().getName(), elementName, getSource(), attributeName, aliasName,
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)));
}
// If the user didn't declare the annotation with an explicit value,
// return the value of the alias.
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
attributeValue = aliasValue;
List<String> aliasNames = this.attributeAliasMap.get(attributeName);
if (aliasNames != null) {
final Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
for (String aliasName : aliasNames) {
if (aliasName != null) {
Object aliasValue = getRawAttributeValue(aliasName);
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
!ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
String elementName = (getAnnotatedElement() != null ? getAnnotatedElement().toString() : "unknown element");
throw new AnnotationConfigurationException(String.format(
"In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " +
"alias '%s' are present with values of [%s] and [%s], but only one is permitted.",
getAnnotationType().getName(), elementName, getSource(), attributeName, aliasName,
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)));
}
// If the user didn't declare the annotation with an explicit value,
// use the value of the alias instead.
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
attributeValue = aliasValue;
}
}
}
}

56
spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java

@ -29,10 +29,10 @@ import java.lang.annotation.Target; @@ -29,10 +29,10 @@ import java.lang.annotation.Target;
*
* <h3>Usage Scenarios</h3>
* <ul>
* <li><strong>Aliases within an annotation</strong>: within a single
* <li><strong>Explicit aliases within an annotation</strong>: within a single
* annotation, {@code @AliasFor} can be declared on a pair of attributes to
* signal that they are interchangeable aliases for each other.</li>
* <li><strong>Alias for attribute in meta-annotation</strong>: if the
* <li><strong>Explicit alias for attribute in meta-annotation</strong>: if the
* {@link #annotation} attribute of {@code @AliasFor} is set to a different
* annotation than the one that declares it, the {@link #attribute} is
* interpreted as an alias for an attribute in a meta-annotation (i.e., an
@ -40,6 +40,11 @@ import java.lang.annotation.Target; @@ -40,6 +40,11 @@ import java.lang.annotation.Target;
* control over exactly which attributes are overridden within an annotation
* hierarchy. In fact, with {@code @AliasFor} it is even possible to declare
* an alias for the {@code value} attribute of a meta-annotation.</li>
* <li><strong>Implicit aliases within an annotation</strong>: if one or
* more attributes within an annotation are declared as explicit
* meta-annotation attribute overrides for the same attribute in the
* meta-annotation, those attributes will be treated as a set of <em>implicit</em>
* aliases for each other, analogous to explicit aliases within an annotation.</li>
* </ul>
*
* <h3>Usage Requirements</h3>
@ -57,31 +62,44 @@ import java.lang.annotation.Target; @@ -57,31 +62,44 @@ import java.lang.annotation.Target;
*
* <h3>Implementation Requirements</h3>
* <ul>
* <li><strong>Aliases within an annotation</strong>:
* <li><strong>Explicit aliases within an annotation</strong>:
* <ol>
* <li>Each attribute that makes up an aliased pair must be annotated with
* {@code @AliasFor}, and either the {@link #attribute} or the {@link #value}
* attribute must reference the <em>other</em> attribute in the pair.</li>
* {@code @AliasFor}, and either {@link #attribute} or {@link #value} must
* reference the <em>other</em> attribute in the pair.</li>
* <li>Aliased attributes must declare the same return type.</li>
* <li>Aliased attributes must declare a default value.</li>
* <li>Aliased attributes must declare the same default value.</li>
* <li>The {@link #annotation} attribute should remain set to the default.</li>
* <li>{@link #annotation} should not be declared.</li>
* </ol>
* </li>
* <li><strong>Alias for attribute in meta-annotation</strong>:
* <li><strong>Explicit alias for attribute in meta-annotation</strong>:
* <ol>
* <li>The attribute that is an alias for an attribute in a meta-annotation
* must be annotated with {@code @AliasFor}, and the {@link #attribute} must
* reference the aliased attribute in the meta-annotation.</li>
* must be annotated with {@code @AliasFor}, and {@link #attribute} must
* reference the attribute in the meta-annotation.</li>
* <li>Aliased attributes must declare the same return type.</li>
* <li>The {@link #annotation} must reference the meta-annotation.</li>
* <li>{@link #annotation} must reference the meta-annotation.</li>
* <li>The referenced meta-annotation must be <em>meta-present</em> on the
* annotation class that declares {@code @AliasFor}.</li>
* </ol>
* </li>
* <li><strong>Implicit aliases within an annotation</strong>:
* <ol>
* <li>Each attribute that belongs to the set of implicit aliases must be
* annotated with {@code @AliasFor}, and {@link #attribute} must reference
* the same attribute in the same meta-annotation.</li>
* <li>Aliased attributes must declare the same return type.</li>
* <li>Aliased attributes must declare a default value.</li>
* <li>Aliased attributes must declare the same default value.</li>
* <li>{@link #annotation} must reference the meta-annotation.</li>
* <li>The referenced meta-annotation must be <em>meta-present</em> on the
* annotation class that declares {@code @AliasFor}.</li>
* </ol>
* </li>
* </ul>
*
* <h3>Example: Aliases within an Annotation</h3>
* <h3>Example: Explicit Aliases within an Annotation</h3>
* <pre class="code"> public &#064;interface ContextConfiguration {
*
* &#064;AliasFor("locations")
@ -93,7 +111,7 @@ import java.lang.annotation.Target; @@ -93,7 +111,7 @@ import java.lang.annotation.Target;
* // ...
* }</pre>
*
* <h3>Example: Alias for Attribute in Meta-annotation</h3>
* <h3>Example: Explicit Alias for Attribute in Meta-annotation</h3>
* <pre class="code"> &#064;ContextConfiguration
* public &#064;interface MyTestConfig {
*
@ -101,6 +119,20 @@ import java.lang.annotation.Target; @@ -101,6 +119,20 @@ import java.lang.annotation.Target;
* String[] xmlFiles();
* }</pre>
*
* <h3>Example: Implicit Aliases within an Annotation</h3>
* <pre class="code"> &#064;ContextConfiguration
* public &#064;interface MyTestConfig {
*
* &#064;AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
* String[] value() default {};
*
* &#064;AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
* String[] groovyScripts() default {};
*
* &#064;AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
* String[] xmlFiles() default {};
* }</pre>
*
* <h3>Spring Annotations Supporting Attribute Aliases</h3>
* <p>As of Spring Framework 4.2, several annotations within core Spring
* have been updated to use {@code @AliasFor} to configure their internal

17
spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java

@ -31,7 +31,6 @@ import org.springframework.core.BridgeMethodResolver; @@ -31,7 +31,6 @@ import org.springframework.core.BridgeMethodResolver;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* General utility methods for finding annotations and meta-annotations on
@ -957,13 +956,21 @@ public class AnnotatedElementUtils { @@ -957,13 +956,21 @@ public class AnnotatedElementUtils {
for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {
String attributeName = attributeMethod.getName();
String aliasedAttributeName = AnnotationUtils.getAliasedAttributeName(attributeMethod,
targetAnnotationType);
List<String> aliases = AnnotationUtils.getAliasedAttributeNames(attributeMethod, targetAnnotationType);
// Explicit annotation attribute override declared via @AliasFor
if (StringUtils.hasText(aliasedAttributeName) && attributes.containsKey(aliasedAttributeName)) {
overrideAttribute(element, annotation, attributes, attributeName, aliasedAttributeName);
if (!aliases.isEmpty()) {
if (aliases.size() != 1) {
throw new IllegalStateException(String.format(
"Alias list for annotation attribute [%s] must contain at most one element: %s",
attributeMethod, aliases));
}
String aliasedAttributeName = aliases.get(0);
if (attributes.containsKey(aliasedAttributeName)) {
overrideAttribute(element, annotation, attributes, attributeName, aliasedAttributeName);
}
}
// Implicit annotation attribute override based on convention
else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
overrideAttribute(element, annotation, attributes, attributeName, attributeName);

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

@ -21,6 +21,7 @@ import java.lang.reflect.AnnotatedElement; @@ -21,6 +21,7 @@ import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.Assert;
@ -422,26 +423,38 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -422,26 +423,38 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
Assert.notNull(expectedType, "expectedType must not be null");
T attributeValue = getAttribute(attributeName, expectedType);
String aliasName = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
T aliasValue = getAttribute(aliasName, expectedType);
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.",
annotationType.getName(), elementName, attributeName, aliasName,
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue));
throw new AnnotationConfigurationException(msg);
}
if (!attributeDeclared) {
attributeValue = aliasValue;
List<String> aliasNames = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
if (aliasNames != null) {
for (String aliasName : aliasNames) {
T aliasValue = getAttribute(aliasName, expectedType);
boolean attributeEmpty = ObjectUtils.isEmpty(attributeValue);
boolean aliasEmpty = ObjectUtils.isEmpty(aliasValue);
if (!attributeEmpty && !aliasEmpty && !ObjectUtils.nullSafeEquals(attributeValue, aliasValue)) {
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.",
annotationType.getName(), elementName, attributeName, aliasName,
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue));
throw new AnnotationConfigurationException(msg);
}
// If we expect an array and the current tracked value is null but the
// current alias value is non-null, then replace the current null value
// with the non-null value (which may be an empty array).
if (expectedType.isArray() && attributeValue == null && aliasValue != null) {
attributeValue = aliasValue;
}
// Else: if we're not expecting an array, we can rely on the behavior of
// ObjectUtils.isEmpty().
else if (attributeEmpty && !aliasEmpty) {
attributeValue = aliasValue;
}
}
assertAttributePresence(attributeName, aliasNames, attributeValue);
}
assertAttributePresence(attributeName, aliasName, attributeValue);
return attributeValue;
}
@ -473,11 +486,11 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> { @@ -473,11 +486,11 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
}
}
private void assertAttributePresence(String attributeName, String aliasName, Object attributeValue) {
private void assertAttributePresence(String attributeName, List<String> aliases, Object attributeValue) {
if (attributeValue == null) {
throw new IllegalArgumentException(String.format(
"Neither attribute '%s' nor its alias '%s' was found in attributes for annotation [%s]",
attributeName, aliasName, this.displayName));
"Neither attribute '%s' nor one of its aliases %s was found in attributes for annotation [%s]",
attributeName, aliases, this.displayName));
}
}

600
spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java

@ -67,7 +67,9 @@ import org.springframework.util.StringUtils; @@ -67,7 +67,9 @@ import org.springframework.util.StringUtils;
*
* <p>An annotation is <em>meta-present</em> on an element if the annotation
* is declared as a meta-annotation on some other annotation which is
* <em>present</em> on the element.
* <em>present</em> on the element. Annotation {@code A} is <em>meta-present</em>
* on another annotation if {@code A} is either <em>directly present</em> or
* <em>meta-present</em> on the other annotation.
*
* <h3>Meta-annotation Support</h3>
* <p>Most {@code find*()} methods and some {@code get*()} methods in this
@ -123,11 +125,14 @@ public abstract class AnnotationUtils { @@ -123,11 +125,14 @@ public abstract class AnnotationUtils {
private static final Map<Class<?>, Boolean> annotatedInterfaceCache =
new ConcurrentReferenceHashMap<Class<?>, Boolean>(256);
private static final Map<AnnotationCacheKey, Boolean> metaPresentCache =
new ConcurrentReferenceHashMap<AnnotationCacheKey, Boolean>(256);
private static final Map<Class<? extends Annotation>, Boolean> synthesizableCache =
new ConcurrentReferenceHashMap<Class<? extends Annotation>, Boolean>(256);
private static final Map<Class<? extends Annotation>, Map<String, String>> attributeAliasesCache =
new ConcurrentReferenceHashMap<Class<? extends Annotation>, Map<String, String>>(256);
private static final Map<Class<? extends Annotation>, Map<String, List<String>>> attributeAliasesCache =
new ConcurrentReferenceHashMap<Class<? extends Annotation>, Map<String, List<String>>>(256);
private static final Map<Class<? extends Annotation>, List<Method>> attributeMethodsCache =
new ConcurrentReferenceHashMap<Class<? extends Annotation>, List<Method>>(256);
@ -643,8 +648,22 @@ public abstract class AnnotationUtils { @@ -643,8 +648,22 @@ public abstract class AnnotationUtils {
* @param annotationType the type of annotation to look for
* @return the first matching annotation, or {@code null} if not found
*/
@SuppressWarnings("unchecked")
public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) {
return findAnnotation(clazz, annotationType, true);
}
/**
* Perform the actual work for {@link #findAnnotation(AnnotatedElement, Class)},
* honoring the {@code synthesize} flag.
* @param clazz the class to look for annotations on; never {@code null}
* @param annotationType the type of annotation to look for
* @param synthesize {@code true} if the result should be
* {@linkplain #synthesizeAnnotation(Annotation) synthesized}
* @return the first matching annotation, or {@code null} if not found
* @since 4.2.1
*/
@SuppressWarnings("unchecked")
private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, boolean synthesize) {
AnnotationCacheKey cacheKey = new AnnotationCacheKey(clazz, annotationType);
A result = (A) findAnnotationCache.get(cacheKey);
if (result == null) {
@ -653,7 +672,7 @@ public abstract class AnnotationUtils { @@ -653,7 +672,7 @@ public abstract class AnnotationUtils {
findAnnotationCache.put(cacheKey, result);
}
}
return synthesizeAnnotation(result, clazz);
return (synthesize ? synthesizeAnnotation(result, clazz) : result);
}
/**
@ -833,6 +852,30 @@ public abstract class AnnotationUtils { @@ -833,6 +852,30 @@ public abstract class AnnotationUtils {
return (clazz.isAnnotationPresent(annotationType) && !isAnnotationDeclaredLocally(annotationType, clazz));
}
/**
* Determine if an annotation of type {@code metaAnnotationType} is
* <em>meta-present</em> on the supplied {@code annotationType}.
* @param annotationType the annotation type to search on; never {@code null}
* @param metaAnnotationType the type of meta-annotation to search for
* @return {@code true} if such an annotation is meta-present
* @since 4.2.1
*/
public static boolean isAnnotationMetaPresent(Class<? extends Annotation> annotationType,
Class<? extends Annotation> metaAnnotationType) {
AnnotationCacheKey cacheKey = new AnnotationCacheKey(annotationType, metaAnnotationType);
Boolean metaPresent = metaPresentCache.get(cacheKey);
if (metaPresent != null) {
return metaPresent.booleanValue();
}
metaPresent = Boolean.FALSE;
if (findAnnotation(annotationType, metaAnnotationType, false) != null) {
metaPresent = Boolean.TRUE;
}
metaPresentCache.put(cacheKey, metaPresent);
return metaPresent.booleanValue();
}
/**
* Determine if the supplied {@link Annotation} is defined in the core JDK
* {@code java.lang.annotation} package.
@ -1363,33 +1406,39 @@ public abstract class AnnotationUtils { @@ -1363,33 +1406,39 @@ public abstract class AnnotationUtils {
}
/**
* Get a map of all attribute alias pairs, declared via {@code @AliasFor}
* Get a map of all attribute aliases declared via {@code @AliasFor}
* in the supplied annotation type.
* <p>The map is keyed by attribute name with each value representing
* the name of the aliased attribute. For each entry {@code [x, y]} in
* the map there will be a corresponding {@code [y, x]} entry in the map.
* a list of names of aliased attributes.
* <p>For <em>explicit</em> alias pairs such as x and y (i.e., where x
* is an {@code @AliasFor("y")} and y is an {@code @AliasFor("x")}, there
* will be two entries in the map: {@code x -> (y)} and {@code y -> (x)}.
* <p>For <em>implicit</em> aliases (i.e., attributes that are declared
* as attribute overrides for the same attribute in the same meta-annotation),
* there will be n entries in the map. For example, if x, y, and z are
* implicit aliases, the map will contain the following entries:
* {@code x -> (y, z)}, {@code y -> (x, z)}, {@code z -> (x, y)}.
* <p>An empty return value implies that the annotation does not declare
* any attribute aliases.
* @param annotationType the annotation type to find attribute aliases in
* @return a map containing attribute alias pairs; never {@code null}
* @return a map containing attribute aliases; never {@code null}
* @since 4.2
*/
static Map<String, String> getAttributeAliasMap(Class<? extends Annotation> annotationType) {
static Map<String, List<String>> getAttributeAliasMap(Class<? extends Annotation> annotationType) {
if (annotationType == null) {
return Collections.emptyMap();
}
Map<String, String> map = attributeAliasesCache.get(annotationType);
Map<String, List<String>> map = attributeAliasesCache.get(annotationType);
if (map != null) {
return map;
}
map = new HashMap<String, String>();
map = new HashMap<String, List<String>>();
for (Method attribute : getAttributeMethods(annotationType)) {
String attributeName = attribute.getName();
String aliasedAttributeName = getAliasedAttributeName(attribute);
if (aliasedAttributeName != null) {
map.put(attributeName, aliasedAttributeName);
List<String> aliasNames = getAliasedAttributeNames(attribute);
if (!aliasNames.isEmpty()) {
map.put(attribute.getName(), aliasNames);
}
}
@ -1420,7 +1469,7 @@ public abstract class AnnotationUtils { @@ -1420,7 +1469,7 @@ public abstract class AnnotationUtils {
synthesizable = Boolean.FALSE;
for (Method attribute : getAttributeMethods(annotationType)) {
if (getAliasedAttributeName(attribute) != null) {
if (!getAliasedAttributeNames(attribute).isEmpty()) {
synthesizable = Boolean.TRUE;
break;
}
@ -1446,184 +1495,85 @@ public abstract class AnnotationUtils { @@ -1446,184 +1495,85 @@ public abstract class AnnotationUtils {
}
/**
* Get the name of the aliased attribute configured via
* {@link AliasFor @AliasFor} on the supplied annotation {@code attribute}.
* <p>This method does not resolve aliases in other annotations. In
* other words, if {@code @AliasFor} is present on the supplied
* {@code attribute} but {@linkplain AliasFor#annotation references an
* annotation} other than {@link Annotation}, this method will return
* {@code null} immediately.
* @param attribute the attribute to find an alias for
* @return the name of the aliased attribute, or {@code null} if not found
* Get the names of the aliased attributes configured via
* {@link AliasFor @AliasFor} for the supplied annotation {@code attribute}.
* <p>This method does not resolve meta-annotation attribute overrides.
* @param attribute the attribute to find aliases for; never {@code null}
* @return the names of the aliased attributes; never {@code null}, though
* potentially <em>empty</em>
* @throws IllegalArgumentException if the supplied attribute method is
* not from an annotation, or if the supplied target type is {@link Annotation}
* {@code null} or not from an annotation
* @throws AnnotationConfigurationException if invalid configuration of
* {@code @AliasFor} is detected
* @since 4.2
* @see #getAliasedAttributeName(Method, Class)
* @see #getAliasedAttributeNames(Method, Class)
*/
static String getAliasedAttributeName(Method attribute) {
return getAliasedAttributeName(attribute, (Class<? extends Annotation>) null);
static List<String> getAliasedAttributeNames(Method attribute) {
return getAliasedAttributeNames(attribute, (Class<? extends Annotation>) null);
}
/**
* Get the name of the aliased attribute configured via
* {@link AliasFor @AliasFor} on the supplied annotation {@code attribute}.
* @param attribute the attribute to find an alias for
* @param targetAnnotationType the type of annotation in which the
* Get the names of the aliased attributes configured via
* {@link AliasFor @AliasFor} for the supplied annotation {@code attribute}.
* <p>If the supplied {@code metaAnnotationType} is non-null, the
* returned list will contain at most one element.
* @param attribute the attribute to find aliases for; never {@code null}
* @param metaAnnotationType the type of meta-annotation in which an
* aliased attribute is allowed to be declared; {@code null} implies
* <em>within the same annotation</em>
* @return the name of the aliased attribute, or {@code null} if not found
* <em>within the same annotation</em> as the supplied attribute
* @return the names of the aliased attributes; never {@code null}, though
* potentially <em>empty</em>
* @throws IllegalArgumentException if the supplied attribute method is
* not from an annotation, or if the supplied target type is {@link Annotation}
* {@code null} or not from an annotation, or if the supplied meta-annotation
* type is {@link Annotation}
* @throws AnnotationConfigurationException if invalid configuration of
* {@code @AliasFor} is detected
* @since 4.2
*/
@SuppressWarnings("unchecked")
static String getAliasedAttributeName(Method attribute, Class<? extends Annotation> targetAnnotationType) {
Class<?> declaringClass = attribute.getDeclaringClass();
Assert.isTrue(declaringClass.isAnnotation(), "attribute method must be from an annotation");
Assert.isTrue(!Annotation.class.equals(targetAnnotationType),
"targetAnnotationType must not be java.lang.annotation.Annotation");
String attributeName = attribute.getName();
AliasFor aliasFor = attribute.getAnnotation(AliasFor.class);
// Nothing to check
if (aliasFor == null) {
return null;
}
static List<String> getAliasedAttributeNames(Method attribute, Class<? extends Annotation> metaAnnotationType) {
Assert.notNull(attribute, "attribute method must not be null");
Assert.isTrue(!Annotation.class.equals(metaAnnotationType),
"metaAnnotationType must not be java.lang.annotation.Annotation");
Class<? extends Annotation> sourceAnnotationType = (Class<? extends Annotation>) declaringClass;
Class<? extends Annotation> aliasedAnnotationType = aliasFor.annotation();
AliasDescriptor descriptor = AliasDescriptor.from(attribute);
boolean searchWithinSameAnnotation = (targetAnnotationType == null);
boolean sameTargetDeclared =
(sourceAnnotationType.equals(aliasedAnnotationType) || Annotation.class.equals(aliasedAnnotationType));
// Explicit alias for a different target meta-annotation?
if (!searchWithinSameAnnotation && !targetAnnotationType.equals(aliasedAnnotationType)) {
return null;
}
String aliasedAttributeName = getAliasedAttributeName(aliasFor, attribute);
if (!StringUtils.hasText(aliasedAttributeName)) {
String msg = String.format(
"@AliasFor declaration on attribute [%s] in annotation [%s] is missing required 'attribute' value.",
attributeName, sourceAnnotationType.getName());
throw new AnnotationConfigurationException(msg);
// No alias declared via @AliasFor?
if (descriptor == null) {
return Collections.emptyList();
}
if (!sameTargetDeclared) {
// Target annotation is not meta-present?
if (findAnnotation(sourceAnnotationType, aliasedAnnotationType) == null) {
String msg = String.format("@AliasFor declaration on attribute [%s] in annotation [%s] declares "
+ "an alias for attribute [%s] in meta-annotation [%s] which is not meta-present.",
attributeName, sourceAnnotationType.getName(), aliasedAttributeName,
aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg);
}
}
else {
aliasedAnnotationType = sourceAnnotationType;
}
// Wrong search scope?
if (searchWithinSameAnnotation && !sameTargetDeclared) {
return null;
}
Method aliasedAttribute;
try {
aliasedAttribute = aliasedAnnotationType.getDeclaredMethod(aliasedAttributeName);
}
catch (NoSuchMethodException ex) {
String msg = String.format(
"Attribute [%s] in annotation [%s] is declared as an @AliasFor nonexistent attribute [%s] in annotation [%s].",
attributeName, sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg, ex);
}
if (sameTargetDeclared) {
AliasFor mirrorAliasFor = aliasedAttribute.getAnnotation(AliasFor.class);
if (mirrorAliasFor == null) {
String msg = String.format("Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s].",
aliasedAttributeName, sourceAnnotationType.getName(), attributeName);
throw new AnnotationConfigurationException(msg);
}
String mirrorAliasedAttributeName = getAliasedAttributeName(mirrorAliasFor, aliasedAttribute);
if (!attributeName.equals(mirrorAliasedAttributeName)) {
String msg = String.format(
"Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s], not [%s].",
aliasedAttributeName, sourceAnnotationType.getName(), attributeName, mirrorAliasedAttributeName);
throw new AnnotationConfigurationException(msg);
// Searching for explicit meta-annotation attribute override?
if (metaAnnotationType != null) {
if (descriptor.isAliasFor(metaAnnotationType)) {
return Collections.singletonList(descriptor.aliasedAttributeName);
}
// Else: explicit attribute override for a different meta-annotation
return Collections.emptyList();
}
Class<?> returnType = attribute.getReturnType();
Class<?> aliasedReturnType = aliasedAttribute.getReturnType();
if (!returnType.equals(aliasedReturnType)) {
String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " +
"and attribute [%s] in annotation [%s] must declare the same return type.", attributeName,
sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg);
// Explicit alias pair?
if (descriptor.isAliasPair) {
return Collections.singletonList(descriptor.aliasedAttributeName);
}
if (sameTargetDeclared) {
Object defaultValue = attribute.getDefaultValue();
Object aliasedDefaultValue = aliasedAttribute.getDefaultValue();
// Else: search for implicit aliases
List<String> aliases = new ArrayList<String>();
for (Method currentAttribute : getAttributeMethods(descriptor.sourceAnnotationType)) {
if ((defaultValue == null) || (aliasedDefaultValue == null)) {
String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " +
"and attribute [%s] in annotation [%s] must declare default values.", attributeName,
sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg);
// An attribute cannot alias itself
if (attribute.equals(currentAttribute)) {
continue;
}
if (!ObjectUtils.nullSafeEquals(defaultValue, aliasedDefaultValue)) {
String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] " +
"and attribute [%s] in annotation [%s] must declare the same default value.", attributeName,
sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg);
// If two attributes override the same attribute in the same meta-annotation,
// they are "implicit" aliases for each other.
AliasDescriptor otherDescriptor = AliasDescriptor.from(currentAttribute);
if (descriptor.equals(otherDescriptor)) {
descriptor.validateAgainst(otherDescriptor);
aliases.add(otherDescriptor.sourceAttributeName);
}
}
return aliasedAttributeName;
}
/**
* Get the name of the aliased attribute configured via the supplied
* {@link AliasFor @AliasFor} annotation on the supplied {@code attribute}.
* <p>This method returns the value of either the {@code attribute}
* or {@code value} attribute of {@code @AliasFor}, ensuring that only
* one of the attributes has been declared.
* @param aliasFor the {@code @AliasFor} annotation from which to retrieve
* the aliased attribute name
* @param attribute the attribute that is annotated with {@code @AliasFor},
* used solely for building an exception message
* @return the name of the aliased attribute, potentially an empty string
* @throws AnnotationConfigurationException if invalid configuration of
* {@code @AliasFor} is detected
* @since 4.2
* @see #getAliasedAttributeName(Method, Class)
*/
private static String getAliasedAttributeName(AliasFor aliasFor, Method attribute) {
String attributeName = aliasFor.attribute();
String value = aliasFor.value();
boolean attributeDeclared = StringUtils.hasText(attributeName);
boolean valueDeclared = StringUtils.hasText(value);
if (attributeDeclared && valueDeclared) {
throw new AnnotationConfigurationException(String.format(
"In @AliasFor declared on attribute [%s] in annotation [%s], attribute 'attribute' and its alias 'value' "
+ "are present with values of [%s] and [%s], but only one is permitted.",
attribute.getName(), attribute.getDeclaringClass().getName(), attributeName, value));
}
return (attributeDeclared ? attributeName : value);
return aliases;
}
/**
@ -1677,6 +1627,7 @@ public abstract class AnnotationUtils { @@ -1677,6 +1627,7 @@ public abstract class AnnotationUtils {
* Determine if the supplied {@code method} is an annotation attribute method.
* @param method the method to check
* @return {@code true} if the method is an attribute method
* @since 4.2
*/
static boolean isAttributeMethod(Method method) {
return (method != null && method.getParameterTypes().length == 0 && method.getReturnType() != void.class);
@ -1686,6 +1637,7 @@ public abstract class AnnotationUtils { @@ -1686,6 +1637,7 @@ public abstract class AnnotationUtils {
* Determine if the supplied method is an "annotationType" method.
* @return {@code true} if the method is an "annotationType" method
* @see Annotation#annotationType()
* @since 4.2
*/
static boolean isAnnotationTypeMethod(Method method) {
return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0);
@ -1723,40 +1675,62 @@ public abstract class AnnotationUtils { @@ -1723,40 +1675,62 @@ public abstract class AnnotationUtils {
Class<? extends Annotation> annotationType = attributes.annotationType();
// Track which attribute values have already been replaced so that we can short
// circuit the search algorithms.
Set<String> valuesAlreadyReplaced = new HashSet<String>();
// Validate @AliasFor configuration
Map<String, String> aliasMap = getAttributeAliasMap(annotationType);
Set<String> validated = new HashSet<String>();
Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
for (String attributeName : aliasMap.keySet()) {
String aliasedAttributeName = aliasMap.get(attributeName);
if (validated.add(attributeName) && validated.add(aliasedAttributeName)) {
Object value = attributes.get(attributeName);
Object aliasedValue = attributes.get(aliasedAttributeName);
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
boolean valuePresent = (value != null && value != DEFAULT_VALUE_PLACEHOLDER);
if (!ObjectUtils.nullSafeEquals(value, aliasedValue) && (value != DEFAULT_VALUE_PLACEHOLDER)
&& (aliasedValue != DEFAULT_VALUE_PLACEHOLDER)) {
String elementAsString = (element == null ? "unknown element" : element.toString());
String msg = String.format(
"In AnnotationAttributes for annotation [%s] declared on [%s], attribute [%s] and its alias [%s] are "
+ "declared with values of [%s] and [%s], but only one declaration is permitted.",
annotationType.getName(), elementAsString, attributeName, aliasedAttributeName,
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue));
throw new AnnotationConfigurationException(msg);
for (String aliasedAttributeName : aliasMap.get(attributeName)) {
if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
continue;
}
// Replace default values with aliased values...
if (value == DEFAULT_VALUE_PLACEHOLDER) {
attributes.put(attributeName,
adaptValue(element, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
}
if (aliasedValue == DEFAULT_VALUE_PLACEHOLDER) {
attributes.put(aliasedAttributeName,
adaptValue(element, value, classValuesAsString, nestedAnnotationsAsMap));
Object aliasedValue = attributes.get(aliasedAttributeName);
boolean aliasPresent = (aliasedValue != null && aliasedValue != DEFAULT_VALUE_PLACEHOLDER);
// Something to validate or replace with an alias?
if (valuePresent || aliasPresent) {
if (valuePresent && aliasPresent) {
// Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
String elementAsString = (element == null ? "unknown element" : element.toString());
String msg = String.format("In AnnotationAttributes for annotation [%s] declared on [%s], "
+ "attribute [%s] and its alias [%s] are declared with values of [%s] and [%s], "
+ "but only one declaration is permitted.", annotationType.getName(),
elementAsString, attributeName, aliasedAttributeName,
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue));
throw new AnnotationConfigurationException(msg);
}
}
else if (aliasPresent) {
// Replace value with aliasedValue
attributes.put(attributeName,
adaptValue(element, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(attributeName);
}
else {
// Replace aliasedValue with value
attributes.put(aliasedAttributeName,
adaptValue(element, value, classValuesAsString, nestedAnnotationsAsMap));
valuesAlreadyReplaced.add(aliasedAttributeName);
}
}
}
}
// Replace any remaining placeholders with actual default values
for (String attributeName : attributes.keySet()) {
if (valuesAlreadyReplaced.contains(attributeName)) {
continue;
}
Object value = attributes.get(attributeName);
if (value == DEFAULT_VALUE_PLACEHOLDER) {
attributes.put(attributeName,
@ -1933,4 +1907,248 @@ public abstract class AnnotationUtils { @@ -1933,4 +1907,248 @@ public abstract class AnnotationUtils {
}
}
/**
* {@code AliasDescriptor} encapsulates the declaration of {@code @AliasFor}
* on a given annotation attribute and includes support for validating
* the configuration of aliases (both explicit and implicit).
* @since 4.2.1
*/
private static class AliasDescriptor {
private final Method sourceAttribute;
private final Class<? extends Annotation> sourceAnnotationType;
private final String sourceAttributeName;
private final Class<? extends Annotation> aliasedAnnotationType;
private final String aliasedAttributeName;
private final boolean isAliasPair;
/**
* Create a new {@code AliasDescriptor} <em>from</em> the declaration
* of {@code @AliasFor} on the supplied annotation attribute and
* validate the configuration of {@code @AliasFor}.
* @param attribute the annotation attribute that is annotated with
* {@code @AliasFor}
* @return a new alias descriptor, or {@code null} if the attribute
* is not annotated with {@code @AliasFor}
* @see #validateAgainst(AliasDescriptor)
*/
public static AliasDescriptor from(Method attribute) {
AliasFor aliasFor = attribute.getAnnotation(AliasFor.class);
if (aliasFor == null) {
return null;
}
AliasDescriptor descriptor = new AliasDescriptor(attribute, aliasFor);
descriptor.validate();
return descriptor;
}
@SuppressWarnings("unchecked")
private AliasDescriptor(Method sourceAttribute, AliasFor aliasFor) {
Class<?> declaringClass = sourceAttribute.getDeclaringClass();
Assert.isTrue(declaringClass.isAnnotation(), "attribute method must be from an annotation");
this.sourceAttribute = sourceAttribute;
this.sourceAnnotationType = (Class<? extends Annotation>) declaringClass;
this.sourceAttributeName = this.sourceAttribute.getName();
this.aliasedAnnotationType = (Annotation.class.equals(aliasFor.annotation()) ? this.sourceAnnotationType
: aliasFor.annotation());
this.aliasedAttributeName = getAliasedAttributeName(aliasFor, this.sourceAttribute);
this.isAliasPair = this.sourceAnnotationType.equals(this.aliasedAnnotationType);
}
private void validate() {
// Target annotation is not meta-present?
if (!this.isAliasPair && !isAnnotationMetaPresent(this.sourceAnnotationType, this.aliasedAnnotationType)) {
String msg = String.format("@AliasFor declaration on attribute [%s] in annotation [%s] declares "
+ "an alias for attribute [%s] in meta-annotation [%s] which is not meta-present.",
this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName,
this.aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg);
}
Method aliasedAttribute;
try {
aliasedAttribute = this.aliasedAnnotationType.getDeclaredMethod(this.aliasedAttributeName);
}
catch (NoSuchMethodException ex) {
String msg = String.format(
"Attribute [%s] in annotation [%s] is declared as an @AliasFor nonexistent attribute [%s] in annotation [%s].",
this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName,
this.aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg, ex);
}
if (this.isAliasPair) {
AliasFor mirrorAliasFor = aliasedAttribute.getAnnotation(AliasFor.class);
if (mirrorAliasFor == null) {
String msg = String.format(
"Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s].",
this.aliasedAttributeName, this.sourceAnnotationType.getName(), this.sourceAttributeName);
throw new AnnotationConfigurationException(msg);
}
String mirrorAliasedAttributeName = getAliasedAttributeName(mirrorAliasFor,
aliasedAttribute);
if (!this.sourceAttributeName.equals(mirrorAliasedAttributeName)) {
String msg = String.format(
"Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s], not [%s].",
this.aliasedAttributeName, this.sourceAnnotationType.getName(), this.sourceAttributeName,
mirrorAliasedAttributeName);
throw new AnnotationConfigurationException(msg);
}
}
Class<?> returnType = this.sourceAttribute.getReturnType();
Class<?> aliasedReturnType = aliasedAttribute.getReturnType();
if (!returnType.equals(aliasedReturnType)) {
String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] "
+ "and attribute [%s] in annotation [%s] must declare the same return type.",
this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName,
this.aliasedAnnotationType.getName());
throw new AnnotationConfigurationException(msg);
}
if (this.isAliasPair) {
validateDefaultValueConfiguration(aliasedAttribute);
}
}
private void validateDefaultValueConfiguration(Method aliasedAttribute) {
Assert.notNull(aliasedAttribute, "aliasedAttribute must not be null");
Object defaultValue = this.sourceAttribute.getDefaultValue();
Object aliasedDefaultValue = aliasedAttribute.getDefaultValue();
if ((defaultValue == null) || (aliasedDefaultValue == null)) {
String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] "
+ "and attribute [%s] in annotation [%s] must declare default values.",
this.sourceAttributeName, this.sourceAnnotationType.getName(), aliasedAttribute.getName(),
aliasedAttribute.getDeclaringClass().getName());
throw new AnnotationConfigurationException(msg);
}
if (!ObjectUtils.nullSafeEquals(defaultValue, aliasedDefaultValue)) {
String msg = String.format("Misconfigured aliases: attribute [%s] in annotation [%s] "
+ "and attribute [%s] in annotation [%s] must declare the same default value.",
this.sourceAttributeName, this.sourceAnnotationType.getName(), aliasedAttribute.getName(),
aliasedAttribute.getDeclaringClass().getName());
throw new AnnotationConfigurationException(msg);
}
}
/**
* Validate this descriptor against the supplied descriptor.
* <p>This method only validates the configuration of default values
* for the two descriptors, since other aspects of the descriptors
* were validated when the descriptors were created.
*/
public void validateAgainst(AliasDescriptor otherDescriptor) {
validateDefaultValueConfiguration(otherDescriptor.sourceAttribute);
}
/**
* Does this descriptor represent an alias for an attribute in the
* supplied {@code targetAnnotationType}?
*/
public boolean isAliasFor(Class<? extends Annotation> targetAnnotationType) {
return targetAnnotationType.equals(this.aliasedAnnotationType);
}
/**
* Determine if this descriptor is logically equal to the supplied
* object.
* <p>Two descriptors are considered equal if the aliases they
* represent are from attributes in one annotation that alias the
* same attribute in a given target annotation.
*/
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof AliasDescriptor)) {
return false;
}
AliasDescriptor that = (AliasDescriptor) other;
if (!this.sourceAnnotationType.equals(that.sourceAnnotationType)) {
return false;
}
if (!this.aliasedAnnotationType.equals(that.aliasedAnnotationType)) {
return false;
}
if (!this.aliasedAttributeName.equals(that.aliasedAttributeName)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = this.sourceAnnotationType.hashCode();
result = 31 * result + this.aliasedAnnotationType.hashCode();
result = 31 * result + this.aliasedAttributeName.hashCode();
return result;
}
@Override
public String toString() {
return String.format("%s: '%s' in @%s is an alias for '%s' in @%s", getClass().getSimpleName(),
this.sourceAttributeName, this.sourceAnnotationType.getName(), this.aliasedAttributeName,
(this.aliasedAnnotationType != null ? this.aliasedAnnotationType.getName() : null));
}
/**
* Get the name of the aliased attribute configured via the supplied
* {@link AliasFor @AliasFor} annotation on the supplied {@code attribute}.
* <p>This method returns the value of either the {@code attribute}
* or {@code value} attribute of {@code @AliasFor}, ensuring that only
* one of the attributes has been declared while simultaneously ensuring
* that at least one of the attributes has been declared.
* @param aliasFor the {@code @AliasFor} annotation from which to retrieve
* the aliased attribute name; never {@code null}
* @param attribute the attribute that is annotated with {@code @AliasFor},
* used solely for building an exception message; never {@code null}
* @return the name of the aliased attribute, never {@code null} or empty
* @throws AnnotationConfigurationException if invalid configuration of
* {@code @AliasFor} is detected
* @since 4.2
* @see AnnotationUtils#getAliasedAttributeNames(Method, Class)
*/
private static String getAliasedAttributeName(AliasFor aliasFor, Method attribute) {
String attributeName = aliasFor.attribute();
String value = aliasFor.value();
boolean attributeDeclared = StringUtils.hasText(attributeName);
boolean valueDeclared = StringUtils.hasText(value);
// Ensure user did not declare both 'value' and 'attribute' in @AliasFor
if (attributeDeclared && valueDeclared) {
throw new AnnotationConfigurationException(String.format(
"In @AliasFor declared on attribute [%s] in annotation [%s], attribute 'attribute' and its alias 'value' "
+ "are present with values of [%s] and [%s], but only one is permitted.",
attribute.getName(), attribute.getDeclaringClass().getName(), attributeName, value));
}
attributeName = (attributeDeclared ? attributeName : value);
// Ensure user declared either 'value' or 'attribute' in @AliasFor
if (!StringUtils.hasText(attributeName)) {
String msg = String.format(
"@AliasFor declaration on attribute [%s] in annotation [%s] is missing required 'attribute' value.",
attribute.getName(), attribute.getDeclaringClass().getName());
throw new AnnotationConfigurationException(msg);
}
return attributeName.trim();
}
}
}

24
spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java

@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.ClassUtils;
@ -87,25 +88,30 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib @@ -87,25 +88,30 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib
Map<String, Object> originalAttributes, Class<? extends Annotation> annotationType) {
Map<String, Object> attributes = new HashMap<String, Object>(originalAttributes);
Map<String, String> attributeAliasMap = getAttributeAliasMap(annotationType);
Map<String, List<String>> attributeAliasMap = getAttributeAliasMap(annotationType);
for (Method attributeMethod : getAttributeMethods(annotationType)) {
String attributeName = attributeMethod.getName();
Object attributeValue = attributes.get(attributeName);
// if attribute not present, check alias
// if attribute not present, check aliases
if (attributeValue == null) {
String aliasName = attributeAliasMap.get(attributeName);
if (aliasName != null) {
Object aliasValue = attributes.get(aliasName);
if (aliasValue != null) {
attributeValue = aliasValue;
attributes.put(attributeName, attributeValue);
List<String> aliasNames = attributeAliasMap.get(attributeName);
if (aliasNames != null) {
for (String aliasName : aliasNames) {
if (aliasName != null) {
Object aliasValue = attributes.get(aliasName);
if (aliasValue != null) {
attributeValue = aliasValue;
attributes.put(attributeName, attributeValue);
break;
}
}
}
}
}
// if alias not present, check default
// if aliases not present, check default
if (attributeValue == null) {
Object defaultValue = getDefaultValue(annotationType, attributeName);
if (defaultValue != null) {

68
spring-core/src/test/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractorTestCase.java

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.junit.Test;
import org.springframework.core.annotation.AnnotationUtilsTests.GroovyImplicitAliasesContextConfigClass;
import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig;
import org.springframework.core.annotation.AnnotationUtilsTests.Location1ImplicitAliasesContextConfigClass;
import org.springframework.core.annotation.AnnotationUtilsTests.Location2ImplicitAliasesContextConfigClass;
import org.springframework.core.annotation.AnnotationUtilsTests.Location3ImplicitAliasesContextConfigClass;
import org.springframework.core.annotation.AnnotationUtilsTests.ValueImplicitAliasesContextConfigClass;
import org.springframework.core.annotation.AnnotationUtilsTests.XmlImplicitAliasesContextConfigClass;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Abstract base class for tests involving concrete implementations of
* {@link AbstractAliasAwareAnnotationAttributeExtractor}.
*
* @author Sam Brannen
* @since 4.2.1
*/
public abstract class AbstractAliasAwareAnnotationAttributeExtractorTestCase {
@Test
public void getAttributeValueForImplicitAliases() throws Exception {
assertGetAttributeValueForImplicitAliases(GroovyImplicitAliasesContextConfigClass.class, "groovyScript");
assertGetAttributeValueForImplicitAliases(XmlImplicitAliasesContextConfigClass.class, "xmlFile");
assertGetAttributeValueForImplicitAliases(ValueImplicitAliasesContextConfigClass.class, "value");
assertGetAttributeValueForImplicitAliases(Location1ImplicitAliasesContextConfigClass.class, "location1");
assertGetAttributeValueForImplicitAliases(Location2ImplicitAliasesContextConfigClass.class, "location2");
assertGetAttributeValueForImplicitAliases(Location3ImplicitAliasesContextConfigClass.class, "location3");
}
private void assertGetAttributeValueForImplicitAliases(Class<?> clazz, String expected) throws Exception {
Method xmlFile = ImplicitAliasesContextConfig.class.getDeclaredMethod("xmlFile");
Method groovyScript = ImplicitAliasesContextConfig.class.getDeclaredMethod("groovyScript");
Method value = ImplicitAliasesContextConfig.class.getDeclaredMethod("value");
AnnotationAttributeExtractor<?> extractor = createExtractorFor(clazz, expected, ImplicitAliasesContextConfig.class);
assertThat(extractor.getAttributeValue(value), is(expected));
assertThat(extractor.getAttributeValue(groovyScript), is(expected));
assertThat(extractor.getAttributeValue(xmlFile), is(expected));
}
protected abstract AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType);
}

122
spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java

@ -323,16 +323,62 @@ public class AnnotatedElementUtilsTests { @@ -323,16 +323,62 @@ public class AnnotatedElementUtilsTests {
}
@Test
public void getMergeAndSynthesizeAnnotationWithAliasedValueComposedAnnotation() {
Class<?> element = AliasedValueComposedContextConfigClass.class;
public void getMergedAnnotationAttributesWithImplicitAliasesInMetaAnnotationOnComposedAnnotation() {
Class<?> element = ComposedImplicitAliasesContextConfigClass.class;
String name = ImplicitAliasesContextConfig.class.getName();
AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name);
String[] expected = new String[] { "A.xml", "B.xml" };
assertNotNull("Should find @ImplicitAliasesContextConfig on " + element.getSimpleName(), attributes);
assertArrayEquals("groovyScripts", expected, attributes.getStringArray("groovyScripts"));
assertArrayEquals("xmlFiles", expected, attributes.getStringArray("xmlFiles"));
assertArrayEquals("locations", expected, attributes.getStringArray("locations"));
assertArrayEquals("value", expected, attributes.getStringArray("value"));
// Verify contracts between utility methods:
assertTrue(isAnnotated(element, name));
}
@Test
public void getMergedAnnotationWithAliasedValueComposedAnnotation() {
assertGetMergedAnnotation(AliasedValueComposedContextConfigClass.class, "test.xml");
}
@Test
public void getMergedAnnotationWithImplicitAliasesForSameAttributeInComposedAnnotation() {
assertGetMergedAnnotation(ImplicitAliasesContextConfigClass1.class, "foo.xml");
assertGetMergedAnnotation(ImplicitAliasesContextConfigClass2.class, "bar.xml");
assertGetMergedAnnotation(ImplicitAliasesContextConfigClass3.class, "baz.xml");
}
private void assertGetMergedAnnotation(Class<?> element, String expected) {
String name = ContextConfig.class.getName();
ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class);
assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), contextConfig);
assertArrayEquals("locations", new String[] { "test.xml" }, contextConfig.locations());
assertArrayEquals("value", new String[] { "test.xml" }, contextConfig.value());
assertArrayEquals("locations", new String[] { expected }, contextConfig.locations());
assertArrayEquals("value", new String[] { expected }, contextConfig.value());
assertArrayEquals("classes", new Class<?>[0], contextConfig.classes());
// Verify contracts between utility methods:
assertTrue(isAnnotated(element, ContextConfig.class.getName()));
assertTrue(isAnnotated(element, name));
}
@Test
public void getMergedAnnotationWithImplicitAliasesInMetaAnnotationOnComposedAnnotation() {
Class<?> element = ComposedImplicitAliasesContextConfigClass.class;
String name = ImplicitAliasesContextConfig.class.getName();
ImplicitAliasesContextConfig config = getMergedAnnotation(element, ImplicitAliasesContextConfig.class);
String[] expected = new String[] { "A.xml", "B.xml" };
assertNotNull("Should find @ImplicitAliasesContextConfig on " + element.getSimpleName(), config);
assertArrayEquals("groovyScripts", expected, config.groovyScripts());
assertArrayEquals("xmlFiles", expected, config.xmlFiles());
assertArrayEquals("locations", expected, config.locations());
assertArrayEquals("value", expected, config.value());
// Verify contracts between utility methods:
assertTrue(isAnnotated(element, name));
}
@Test
@ -517,11 +563,11 @@ public class AnnotatedElementUtilsTests { @@ -517,11 +563,11 @@ public class AnnotatedElementUtilsTests {
@Test
public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
String[] expected = new String[] { "com.example.app.test" };
Class<?> element = TestComponentScanClass.class;
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
assertNotNull("Should find @ComponentScan on " + element, attributes);
assertArrayEquals("basePackages for " + element, new String[] { "com.example.app.test" },
attributes.getStringArray("basePackages"));
assertArrayEquals("basePackages for " + element, expected, attributes.getStringArray("basePackages"));
Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class);
assertNotNull(excludeFilters);
@ -530,6 +576,22 @@ public class AnnotatedElementUtilsTests { @@ -530,6 +576,22 @@ public class AnnotatedElementUtilsTests {
assertEquals(asList("*Test", "*Tests"), patterns);
}
/**
* This test ensures that {@link AnnotationUtils#postProcessAnnotationAttributes}
* uses {@code ObjectUtils.nullSafeEquals()} to check for equality between annotation
* attributes since attributes may be arrays.
*/
@Test
public void findMergedAnnotationAttributesOnClassWithBothAttributesOfAnAliasPairDeclared() {
String[] expected = new String[] { "com.example.app.test" };
Class<?> element = ComponentScanWithBasePackagesAndValueAliasClass.class;
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
assertNotNull("Should find @ComponentScan on " + element, attributes);
assertArrayEquals("value: ", expected, attributes.getStringArray("value"));
assertArrayEquals("basePackages: ", expected, attributes.getStringArray("basePackages"));
}
@Test
public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() {
final String[] EMPTY = new String[] {};
@ -716,6 +778,28 @@ public class AnnotatedElementUtilsTests { @@ -716,6 +778,28 @@ public class AnnotatedElementUtilsTests {
String[] locations();
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface ImplicitAliasesContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
String[] groovyScripts() default {};
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
String[] xmlFiles() default {};
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
String[] locations() default {};
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
String[] value() default {};
}
@ImplicitAliasesContextConfig(xmlFiles = { "A.xml", "B.xml" })
@Retention(RetentionPolicy.RUNTIME)
@interface ComposedImplicitAliasesContextConfig {
}
/**
* Invalid because the configuration declares a value for 'value' and
* requires a value for the aliased 'locations'. So we likely end up with
@ -762,6 +846,10 @@ public class AnnotatedElementUtilsTests { @@ -762,6 +846,10 @@ public class AnnotatedElementUtilsTests {
@Retention(RetentionPolicy.RUNTIME)
@interface ComponentScan {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Filter[] excludeFilters() default {};
@ -928,6 +1016,22 @@ public class AnnotatedElementUtilsTests { @@ -928,6 +1016,22 @@ public class AnnotatedElementUtilsTests {
static class AliasedValueComposedContextConfigClass {
}
@ImplicitAliasesContextConfig("foo.xml")
static class ImplicitAliasesContextConfigClass1 {
}
@ImplicitAliasesContextConfig(locations = "bar.xml")
static class ImplicitAliasesContextConfigClass2 {
}
@ImplicitAliasesContextConfig(xmlFiles = "baz.xml")
static class ImplicitAliasesContextConfigClass3 {
}
@ComposedImplicitAliasesContextConfig
static class ComposedImplicitAliasesContextConfigClass {
}
@InvalidAliasedComposedContextConfig(xmlConfigFiles = "test.xml")
static class InvalidAliasedComposedContextConfigClass {
}
@ -936,6 +1040,10 @@ public class AnnotatedElementUtilsTests { @@ -936,6 +1040,10 @@ public class AnnotatedElementUtilsTests {
static class AliasedComposedContextConfigAndTestPropSourceClass {
}
@ComponentScan(value = "com.example.app.test", basePackages = "com.example.app.test")
static class ComponentScanWithBasePackagesAndValueAliasClass {
}
@TestComponentScan(packages = "com.example.app.test")
static class TestComponentScanClass {
}

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

@ -18,11 +18,16 @@ package org.springframework.core.annotation; @@ -18,11 +18,16 @@ package org.springframework.core.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.annotation.AnnotationUtilsTests.ContextConfig;
import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
@ -36,7 +41,7 @@ import static org.junit.Assert.*; @@ -36,7 +41,7 @@ import static org.junit.Assert.*;
*/
public class AnnotationAttributesTests {
private final AnnotationAttributes attributes = new AnnotationAttributes();
private AnnotationAttributes attributes = new AnnotationAttributes();
@Rule
public final ExpectedException exception = ExpectedException.none();
@ -156,21 +161,56 @@ public class AnnotationAttributesTests { @@ -156,21 +161,56 @@ public class AnnotationAttributesTests {
@Test
public void getAliasedString() {
final String value = "metaverse";
attributes.clear();
attributes.put("name", value);
assertEquals(value, getAliasedString("name"));
assertEquals(value, getAliasedString("value"));
attributes.clear();
attributes.put("value", value);
assertEquals(value, getAliasedString("name"));
assertEquals(value, getAliasedString("value"));
attributes.clear();
attributes.put("name", "metaverse");
assertEquals("metaverse", getAliasedString("name"));
assertEquals("metaverse", getAliasedString("value"));
attributes.put("name", value);
attributes.put("value", value);
assertEquals(value, getAliasedString("name"));
assertEquals(value, getAliasedString("value"));
}
@Test
public void getAliasedStringWithImplicitAliases() {
final String value = "metaverse";
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
attributes.put("value", value);
aliases.stream().forEach(alias -> assertEquals(value, getAliasedStringWithImplicitAliases(alias)));
attributes.clear();
attributes.put("value", "metaverse");
assertEquals("metaverse", getAliasedString("name"));
assertEquals("metaverse", getAliasedString("value"));
attributes.put("location1", value);
aliases.stream().forEach(alias -> assertEquals(value, getAliasedStringWithImplicitAliases(alias)));
attributes.clear();
attributes.put("name", "metaverse");
attributes.put("value", "metaverse");
assertEquals("metaverse", getAliasedString("name"));
assertEquals("metaverse", getAliasedString("value"));
attributes.put("value", value);
attributes.put("location1", value);
attributes.put("xmlFile", value);
attributes.put("groovyScript", value);
aliases.stream().forEach(alias -> assertEquals(value, getAliasedStringWithImplicitAliases(alias)));
}
@Test
public void getAliasedStringWithImplicitAliasesWithMissingAliasedAttributes() {
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("Neither attribute 'value' nor one of its aliases ["));
aliases.stream().forEach(alias -> exception.expectMessage(containsString(alias)));
exception.expectMessage(endsWith("] was found in attributes for annotation [" + ImplicitAliasesContextConfig.class.getName() + "]"));
getAliasedStringWithImplicitAliases("value");
}
@Test
@ -185,7 +225,7 @@ public class AnnotationAttributesTests { @@ -185,7 +225,7 @@ public class AnnotationAttributesTests {
@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]"));
exception.expectMessage(equalTo("Neither attribute 'name' nor one of its aliases [value] was found in attributes for annotation [unknown]"));
getAliasedString("name");
}
@ -211,71 +251,135 @@ public class AnnotationAttributesTests { @@ -211,71 +251,135 @@ public class AnnotationAttributesTests {
return attrs.getAliasedString(attributeName, Scope.class, null);
}
private String getAliasedStringWithImplicitAliases(String attributeName) {
return this.attributes.getAliasedString(attributeName, ImplicitAliasesContextConfig.class, null);
}
@Test
public void getAliasedStringArray() {
final String[] INPUT = new String[] { "test.xml" };
final String[] EMPTY = new String[0];
attributes.clear();
attributes.put("locations", INPUT);
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
attributes.put("location", INPUT);
assertArrayEquals(INPUT, getAliasedStringArray("location"));
assertArrayEquals(INPUT, getAliasedStringArray("value"));
attributes.clear();
attributes.put("value", INPUT);
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
assertArrayEquals(INPUT, getAliasedStringArray("location"));
assertArrayEquals(INPUT, getAliasedStringArray("value"));
attributes.clear();
attributes.put("locations", INPUT);
attributes.put("location", INPUT);
attributes.put("value", INPUT);
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
assertArrayEquals(INPUT, getAliasedStringArray("location"));
assertArrayEquals(INPUT, getAliasedStringArray("value"));
attributes.clear();
attributes.put("locations", INPUT);
attributes.put("location", INPUT);
attributes.put("value", EMPTY);
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
assertArrayEquals(INPUT, getAliasedStringArray("location"));
assertArrayEquals(INPUT, getAliasedStringArray("value"));
attributes.clear();
attributes.put("locations", EMPTY);
attributes.put("location", EMPTY);
attributes.put("value", INPUT);
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
assertArrayEquals(INPUT, getAliasedStringArray("location"));
assertArrayEquals(INPUT, getAliasedStringArray("value"));
attributes.clear();
attributes.put("locations", EMPTY);
attributes.put("location", EMPTY);
attributes.put("value", EMPTY);
assertArrayEquals(EMPTY, getAliasedStringArray("locations"));
assertArrayEquals(EMPTY, getAliasedStringArray("location"));
assertArrayEquals(EMPTY, getAliasedStringArray("value"));
}
@Test
public void getAliasedStringArrayWithImplicitAliases() {
final String[] INPUT = new String[] { "test.xml" };
final String[] EMPTY = new String[0];
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
attributes.put("location1", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("value", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", INPUT);
attributes.put("value", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", INPUT);
attributes.put("value", EMPTY);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", EMPTY);
attributes.put("value", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", EMPTY);
attributes.put("value", EMPTY);
aliases.stream().forEach(alias -> assertArrayEquals(EMPTY, getAliasedStringArrayWithImplicitAliases(alias)));
}
@Test
public void getAliasedStringArrayWithImplicitAliasesWithMissingAliasedAttributes() {
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("Neither attribute 'value' nor one of its aliases ["));
aliases.stream().forEach(alias -> exception.expectMessage(containsString(alias)));
exception.expectMessage(endsWith("] was found in attributes for annotation [" + ImplicitAliasesContextConfig.class.getName() + "]"));
getAliasedStringArrayWithImplicitAliases("value");
}
@Test
public void getAliasedStringArrayWithMissingAliasedAttributes() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(equalTo("Neither attribute 'locations' nor its alias 'value' was found in attributes for annotation [unknown]"));
getAliasedStringArray("locations");
exception.expectMessage(equalTo("Neither attribute 'location' nor one of its aliases [value] was found in attributes for annotation [unknown]"));
getAliasedStringArray("location");
}
@Test
public void getAliasedStringArrayWithDifferentAliasedValues() {
attributes.put("locations", new String[] { "1.xml" });
attributes.put("location", new String[] { "1.xml" });
attributes.put("value", new String[] { "2.xml" });
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("In annotation [" + ContextConfig.class.getName() + "]"));
exception.expectMessage(containsString("attribute [locations] and its alias [value]"));
exception.expectMessage(containsString("attribute [location] and its alias [value]"));
exception.expectMessage(containsString("[{1.xml}] and [{2.xml}]"));
exception.expectMessage(containsString("but only one is permitted"));
getAliasedStringArray("locations");
getAliasedStringArray("location");
}
private String[] getAliasedStringArray(String attributeName) {
// Note: even though the attributes we test against here are of type
// String instead of String[], it doesn't matter... since
// AnnotationAttributes does not validate the actual return type of
// attributes in the annotation.
return attributes.getAliasedStringArray(attributeName, ContextConfig.class, null);
}
private String[] getAliasedStringArrayWithImplicitAliases(String attributeName) {
// Note: even though the attributes we test against here are of type
// String instead of String[], it doesn't matter... since
// AnnotationAttributes does not validate the actual return type of
// attributes in the annotation.
return this.attributes.getAliasedStringArray(attributeName, ImplicitAliasesContextConfig.class, null);
}
@Test
public void getAliasedClassArray() {
final Class<?>[] INPUT = new Class<?>[] { String.class };
@ -316,10 +420,46 @@ public class AnnotationAttributesTests { @@ -316,10 +420,46 @@ public class AnnotationAttributesTests {
assertArrayEquals(EMPTY, getAliasedClassArray("value"));
}
@Test
public void getAliasedClassArrayWithImplicitAliases() {
final Class<?>[] INPUT = new Class<?>[] { String.class };
final Class<?>[] EMPTY = new Class<?>[0];
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
attributes.put("location1", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("value", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", INPUT);
attributes.put("value", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", INPUT);
attributes.put("value", EMPTY);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", EMPTY);
attributes.put("value", INPUT);
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
attributes.clear();
attributes.put("location1", EMPTY);
attributes.put("value", EMPTY);
aliases.stream().forEach(alias -> assertArrayEquals(EMPTY, getAliasedClassArrayWithImplicitAliases(alias)));
}
@Test
public void getAliasedClassArrayWithMissingAliasedAttributes() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(equalTo("Neither attribute 'classes' nor its alias 'value' was found in attributes for annotation [unknown]"));
exception.expectMessage(equalTo("Neither attribute 'classes' nor one of its aliases [value] was found in attributes for annotation [unknown]"));
getAliasedClassArray("classes");
}
@ -341,6 +481,14 @@ public class AnnotationAttributesTests { @@ -341,6 +481,14 @@ public class AnnotationAttributesTests {
return attributes.getAliasedClassArray(attributeName, Filter.class, null);
}
private Class<?>[] getAliasedClassArrayWithImplicitAliases(String attributeName) {
// Note: even though the attributes we test against here are of type
// String instead of Class<?>[], it doesn't matter... since
// AnnotationAttributes does not validate the actual return type of
// attributes in the annotation.
return this.attributes.getAliasedClassArray(attributeName, ImplicitAliasesContextConfig.class, null);
}
enum Color {
RED, WHITE, BLUE
@ -362,19 +510,6 @@ public class AnnotationAttributesTests { @@ -362,19 +510,6 @@ public class AnnotationAttributesTests {
static class FilteredClass {
}
/**
* Mock of {@code org.springframework.test.context.ContextConfiguration}.
*/
@Retention(RetentionPolicy.RUNTIME)
@interface ContextConfig {
@AliasFor(attribute = "locations")
String value() default "";
@AliasFor(attribute = "value")
String locations() default "";
}
/**
* Mock of {@code org.springframework.context.annotation.Scope}.
*/

379
spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java

@ -22,14 +22,14 @@ import java.lang.annotation.Repeatable; @@ -22,14 +22,14 @@ import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
@ -38,6 +38,7 @@ import org.springframework.core.Ordered; @@ -38,6 +38,7 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import static java.util.Arrays.*;
import static java.util.stream.Collectors.*;
@ -56,10 +57,31 @@ import static org.springframework.core.annotation.AnnotationUtils.*; @@ -56,10 +57,31 @@ import static org.springframework.core.annotation.AnnotationUtils.*;
*/
public class AnnotationUtilsTests {
static void clearCaches() {
clearCache("findAnnotationCache", "annotatedInterfaceCache", "metaPresentCache", "synthesizableCache",
"attributeAliasesCache", "attributeMethodsCache");
}
static void clearCache(String... cacheNames) {
stream(cacheNames).forEach(cacheName -> getCache(cacheName).clear());
}
static Map<?, ?> getCache(String cacheName) {
Field field = ReflectionUtils.findField(AnnotationUtils.class, cacheName);
ReflectionUtils.makeAccessible(field);
return (Map<?, ?>) ReflectionUtils.getField(field, null);
}
@Rule
public final ExpectedException exception = ExpectedException.none();
@Before
public void clearCachesBeforeTests() {
clearCaches();
}
@Test
public void findMethodAnnotationOnLeaf() throws Exception {
Method m = Leaf.class.getMethod("annotatedOnLeaf");
@ -308,7 +330,7 @@ public class AnnotationUtilsTests { @@ -308,7 +330,7 @@ public class AnnotationUtilsTests {
@Test
public void findAnnotationDeclaringClassForTypesWithSingleCandidateType() {
// no class-level annotation
List<Class<? extends Annotation>> transactionalCandidateList = Arrays.<Class<? extends Annotation>> asList(Transactional.class);
List<Class<? extends Annotation>> transactionalCandidateList = asList(Transactional.class);
assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedInterface.class));
assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedClass.class));
@ -323,7 +345,7 @@ public class AnnotationUtilsTests { @@ -323,7 +345,7 @@ public class AnnotationUtilsTests {
// non-inherited class-level annotation; note: @Order is not inherited,
// but findAnnotationDeclaringClassForTypes() should still find it on classes.
List<Class<? extends Annotation>> orderCandidateList = Arrays.<Class<? extends Annotation>> asList(Order.class);
List<Class<? extends Annotation>> orderCandidateList = asList(Order.class);
assertEquals(NonInheritedAnnotationInterface.class,
findAnnotationDeclaringClassForTypes(orderCandidateList, NonInheritedAnnotationInterface.class));
assertNull(findAnnotationDeclaringClassForTypes(orderCandidateList, SubNonInheritedAnnotationInterface.class));
@ -335,7 +357,7 @@ public class AnnotationUtilsTests { @@ -335,7 +357,7 @@ public class AnnotationUtilsTests {
@Test
public void findAnnotationDeclaringClassForTypesWithMultipleCandidateTypes() {
List<Class<? extends Annotation>> candidates = Arrays.<Class<? extends Annotation>> asList(Transactional.class, Order.class);
List<Class<? extends Annotation>> candidates = asList(Transactional.class, Order.class);
// no class-level annotation
assertNull(findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedInterface.class));
@ -461,7 +483,7 @@ public class AnnotationUtilsTests { @@ -461,7 +483,7 @@ public class AnnotationUtilsTests {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("attribute 'value' and its alias 'path'"));
exception.expectMessage(containsString("values of [/enigma] and [/test]"));
exception.expectMessage(containsString("but only one is permitted"));
exception.expectMessage(endsWith("but only one is permitted."));
getAnnotationAttributes(webMapping);
}
@ -524,21 +546,21 @@ public class AnnotationUtilsTests { @@ -524,21 +546,21 @@ public class AnnotationUtilsTests {
Set<MyRepeatable> annotations = getRepeatableAnnotations(method, MyRepeatable.class, MyRepeatableContainer.class);
assertNotNull(annotations);
List<String> values = annotations.stream().map(MyRepeatable::value).collect(toList());
assertThat(values, is(Arrays.asList("A", "B", "C", "meta1")));
assertThat(values, is(asList("A", "B", "C", "meta1")));
}
@Test
public void getRepeatableAnnotationsDeclaredOnClassWithMissingAttributeAliasDeclaration() throws Exception {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [value] in"));
exception.expectMessage(startsWith("Attribute [value] in"));
exception.expectMessage(containsString(BrokenContextConfig.class.getName()));
exception.expectMessage(containsString("must be declared as an @AliasFor [location]"));
exception.expectMessage(endsWith("must be declared as an @AliasFor [location]."));
getRepeatableAnnotations(BrokenConfigHierarchyTestCase.class, BrokenContextConfig.class, BrokenHierarchy.class);
}
@Test
public void getRepeatableAnnotationsDeclaredOnClassWithAttributeAliases() throws Exception {
final List<String> expectedLocations = Arrays.asList("A", "B");
final List<String> expectedLocations = asList("A", "B");
Set<ContextConfig> annotations = getRepeatableAnnotations(ConfigHierarchyTestCase.class, ContextConfig.class, null);
assertNotNull(annotations);
@ -556,8 +578,8 @@ public class AnnotationUtilsTests { @@ -556,8 +578,8 @@ public class AnnotationUtilsTests {
@Test
public void getRepeatableAnnotationsDeclaredOnClass() {
final List<String> expectedValuesJava = Arrays.asList("A", "B", "C");
final List<String> expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1");
final List<String> expectedValuesJava = asList("A", "B", "C");
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
// Java 8
MyRepeatable[] array = MyRepeatableClass.class.getAnnotationsByType(MyRepeatable.class);
@ -581,8 +603,8 @@ public class AnnotationUtilsTests { @@ -581,8 +603,8 @@ public class AnnotationUtilsTests {
@Test
public void getRepeatableAnnotationsDeclaredOnSuperclass() {
final Class<?> clazz = SubMyRepeatableClass.class;
final List<String> expectedValuesJava = Arrays.asList("A", "B", "C");
final List<String> expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1");
final List<String> expectedValuesJava = asList("A", "B", "C");
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
// Java 8
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
@ -606,8 +628,8 @@ public class AnnotationUtilsTests { @@ -606,8 +628,8 @@ public class AnnotationUtilsTests {
@Test
public void getRepeatableAnnotationsDeclaredOnClassAndSuperclass() {
final Class<?> clazz = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class;
final List<String> expectedValuesJava = Arrays.asList("X", "Y", "Z");
final List<String> expectedValuesSpring = Arrays.asList("X", "Y", "Z", "meta2");
final List<String> expectedValuesJava = asList("X", "Y", "Z");
final List<String> expectedValuesSpring = asList("X", "Y", "Z", "meta2");
// Java 8
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
@ -631,8 +653,8 @@ public class AnnotationUtilsTests { @@ -631,8 +653,8 @@ public class AnnotationUtilsTests {
@Test
public void getRepeatableAnnotationsDeclaredOnMultipleSuperclasses() {
final Class<?> clazz = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class;
final List<String> expectedValuesJava = Arrays.asList("X", "Y", "Z");
final List<String> expectedValuesSpring = Arrays.asList("X", "Y", "Z", "meta2");
final List<String> expectedValuesJava = asList("X", "Y", "Z");
final List<String> expectedValuesSpring = asList("X", "Y", "Z", "meta2");
// Java 8
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
@ -655,8 +677,8 @@ public class AnnotationUtilsTests { @@ -655,8 +677,8 @@ public class AnnotationUtilsTests {
@Test
public void getDeclaredRepeatableAnnotationsDeclaredOnClass() {
final List<String> expectedValuesJava = Arrays.asList("A", "B", "C");
final List<String> expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1");
final List<String> expectedValuesJava = asList("A", "B", "C");
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
// Java 8
MyRepeatable[] array = MyRepeatableClass.class.getDeclaredAnnotationsByType(MyRepeatable.class);
@ -698,16 +720,45 @@ public class AnnotationUtilsTests { @@ -698,16 +720,45 @@ public class AnnotationUtilsTests {
}
@Test
public void getAliasedAttributeNameFromWrongTargetAnnotation() throws Exception {
public void getAliasedAttributeNamesFromWrongTargetAnnotation() throws Exception {
Method attribute = AliasedComposedContextConfig.class.getDeclaredMethod("xmlConfigFile");
assertNull("xmlConfigFile is not an alias for @Component.",
getAliasedAttributeName(attribute, Component.class));
assertThat("xmlConfigFile is not an alias for @Component.",
getAliasedAttributeNames(attribute, Component.class), is(empty()));
}
@Test
public void getAliasedAttributeNamesForNonAliasedAttribute() throws Exception {
Method nonAliasedAttribute = ImplicitAliasesContextConfig.class.getDeclaredMethod("nonAliasedAttribute");
assertThat(getAliasedAttributeNames(nonAliasedAttribute, ContextConfig.class), is(empty()));
}
@Test
public void getAliasedAttributeNameFromAliasedComposedAnnotation() throws Exception {
public void getAliasedAttributeNamesFromAliasedComposedAnnotation() throws Exception {
Method attribute = AliasedComposedContextConfig.class.getDeclaredMethod("xmlConfigFile");
assertEquals("location", getAliasedAttributeName(attribute, ContextConfig.class));
assertEquals(asList("location"), getAliasedAttributeNames(attribute, ContextConfig.class));
}
@Test
public void getAliasedAttributeNamesFromComposedAnnotationWithImplicitAliases() throws Exception {
Method xmlFile = ImplicitAliasesContextConfig.class.getDeclaredMethod("xmlFile");
Method groovyScript = ImplicitAliasesContextConfig.class.getDeclaredMethod("groovyScript");
Method value = ImplicitAliasesContextConfig.class.getDeclaredMethod("value");
Method location1 = ImplicitAliasesContextConfig.class.getDeclaredMethod("location1");
Method location2 = ImplicitAliasesContextConfig.class.getDeclaredMethod("location2");
Method location3 = ImplicitAliasesContextConfig.class.getDeclaredMethod("location3");
// Meta-annotation attribute overrides
assertEquals(asList("location"), getAliasedAttributeNames(xmlFile, ContextConfig.class));
assertEquals(asList("location"), getAliasedAttributeNames(groovyScript, ContextConfig.class));
assertEquals(asList("location"), getAliasedAttributeNames(value, ContextConfig.class));
// Implicit Aliases
assertThat(getAliasedAttributeNames(xmlFile), containsInAnyOrder("value", "groovyScript", "location1", "location2", "location3"));
assertThat(getAliasedAttributeNames(groovyScript), containsInAnyOrder("value", "xmlFile", "location1", "location2", "location3"));
assertThat(getAliasedAttributeNames(value), containsInAnyOrder("xmlFile", "groovyScript", "location1", "location2", "location3"));
assertThat(getAliasedAttributeNames(location1), containsInAnyOrder("xmlFile", "groovyScript", "value", "location2", "location3"));
assertThat(getAliasedAttributeNames(location2), containsInAnyOrder("xmlFile", "groovyScript", "value", "location1", "location3"));
assertThat(getAliasedAttributeNames(location3), containsInAnyOrder("xmlFile", "groovyScript", "value", "location1", "location2"));
}
@Test
@ -746,9 +797,9 @@ public class AnnotationUtilsTests { @@ -746,9 +797,9 @@ public class AnnotationUtilsTests {
public void synthesizeAnnotationWhereAliasForIsMissingAttributeDeclaration() throws Exception {
AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation(AliasForWithMissingAttributeDeclaration.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("@AliasFor declaration on attribute [foo] in annotation"));
exception.expectMessage(startsWith("@AliasFor declaration on attribute [foo] in annotation"));
exception.expectMessage(containsString(AliasForWithMissingAttributeDeclaration.class.getName()));
exception.expectMessage(containsString("is missing required 'attribute' value"));
exception.expectMessage(endsWith("is missing required 'attribute' value."));
synthesizeAnnotation(annotation);
}
@ -756,10 +807,10 @@ public class AnnotationUtilsTests { @@ -756,10 +807,10 @@ public class AnnotationUtilsTests {
public void synthesizeAnnotationWhereAliasForHasDuplicateAttributeDeclaration() throws Exception {
AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation(AliasForWithDuplicateAttributeDeclaration.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("In @AliasFor declared on attribute [foo] in annotation"));
exception.expectMessage(startsWith("In @AliasFor declared on attribute [foo] in annotation"));
exception.expectMessage(containsString(AliasForWithDuplicateAttributeDeclaration.class.getName()));
exception.expectMessage(containsString("attribute 'attribute' and its alias 'value' are present with values of [baz] and [bar]"));
exception.expectMessage(containsString("but only one is permitted"));
exception.expectMessage(endsWith("but only one is permitted."));
synthesizeAnnotation(annotation);
}
@ -767,7 +818,7 @@ public class AnnotationUtilsTests { @@ -767,7 +818,7 @@ public class AnnotationUtilsTests {
public void synthesizeAnnotationWithAttributeAliasForNonexistentAttribute() throws Exception {
AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [foo] in"));
exception.expectMessage(startsWith("Attribute [foo] in"));
exception.expectMessage(containsString(AliasForNonexistentAttribute.class.getName()));
exception.expectMessage(containsString("is declared as an @AliasFor nonexistent attribute [bar]"));
synthesizeAnnotation(annotation);
@ -778,9 +829,9 @@ public class AnnotationUtilsTests { @@ -778,9 +829,9 @@ public class AnnotationUtilsTests {
AliasForWithoutMirroredAliasFor annotation =
AliasForWithoutMirroredAliasForClass.class.getAnnotation(AliasForWithoutMirroredAliasFor.class);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [bar] in"));
exception.expectMessage(startsWith("Attribute [bar] in"));
exception.expectMessage(containsString(AliasForWithoutMirroredAliasFor.class.getName()));
exception.expectMessage(containsString("must be declared as an @AliasFor [foo]"));
exception.expectMessage(endsWith("must be declared as an @AliasFor [foo]."));
synthesizeAnnotation(annotation);
}
@ -789,12 +840,8 @@ public class AnnotationUtilsTests { @@ -789,12 +840,8 @@ public class AnnotationUtilsTests {
AliasForWithMirroredAliasForWrongAttribute annotation =
AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation(AliasForWithMirroredAliasForWrongAttribute.class);
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
// In other words, we can't be certain which type of exception message we'll get,
// so we allow for both possibilities.
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("Attribute [bar] in"));
exception.expectMessage(startsWith("Attribute [bar] in"));
exception.expectMessage(containsString(AliasForWithMirroredAliasForWrongAttribute.class.getName()));
exception.expectMessage(either(containsString("must be declared as an @AliasFor [foo], not [quux]")).
or(containsString("is declared as an @AliasFor nonexistent attribute [quux]")));
@ -808,13 +855,9 @@ public class AnnotationUtilsTests { @@ -808,13 +855,9 @@ public class AnnotationUtilsTests {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Misconfigured aliases"));
exception.expectMessage(containsString(AliasForAttributeOfDifferentType.class.getName()));
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
// In other words, we don't know if "foo" or "bar" will come first.
exception.expectMessage(containsString("attribute [foo]"));
exception.expectMessage(containsString("attribute [bar]"));
exception.expectMessage(containsString("must declare the same return type"));
exception.expectMessage(endsWith("must declare the same return type."));
synthesizeAnnotation(annotation);
}
@ -825,13 +868,9 @@ public class AnnotationUtilsTests { @@ -825,13 +868,9 @@ public class AnnotationUtilsTests {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Misconfigured aliases"));
exception.expectMessage(containsString(AliasForWithMissingDefaultValues.class.getName()));
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
// In other words, we don't know if "foo" or "bar" will come first.
exception.expectMessage(containsString("attribute [foo]"));
exception.expectMessage(containsString("attribute [bar]"));
exception.expectMessage(containsString("must declare default values"));
exception.expectMessage(containsString("attribute [foo] in annotation"));
exception.expectMessage(containsString("attribute [bar] in annotation"));
exception.expectMessage(endsWith("must declare default values."));
synthesizeAnnotation(annotation);
}
@ -842,13 +881,9 @@ public class AnnotationUtilsTests { @@ -842,13 +881,9 @@ public class AnnotationUtilsTests {
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Misconfigured aliases"));
exception.expectMessage(containsString(AliasForAttributeWithDifferentDefaultValue.class.getName()));
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
// reflection, we cannot make the test dependent on any specific ordering.
// In other words, we don't know if "foo" or "bar" will come first.
exception.expectMessage(containsString("attribute [foo]"));
exception.expectMessage(containsString("attribute [bar]"));
exception.expectMessage(containsString("must declare the same default value"));
exception.expectMessage(containsString("attribute [foo] in annotation"));
exception.expectMessage(containsString("attribute [bar] in annotation"));
exception.expectMessage(endsWith("must declare the same default value."));
synthesizeAnnotation(annotation);
}
@ -887,13 +922,91 @@ public class AnnotationUtilsTests { @@ -887,13 +922,91 @@ public class AnnotationUtilsTests {
assertEquals("actual value attribute: ", "/test", synthesizedWebMapping2.value());
}
@Test
public void synthesizeAnnotationWithImplicitAliases() throws Exception {
assertAnnotationSynthesisWithImplicitAliases(ValueImplicitAliasesContextConfigClass.class, "value");
assertAnnotationSynthesisWithImplicitAliases(Location1ImplicitAliasesContextConfigClass.class, "location1");
assertAnnotationSynthesisWithImplicitAliases(XmlImplicitAliasesContextConfigClass.class, "xmlFile");
assertAnnotationSynthesisWithImplicitAliases(GroovyImplicitAliasesContextConfigClass.class, "groovyScript");
}
private void assertAnnotationSynthesisWithImplicitAliases(Class<?> clazz, String expected) throws Exception {
ImplicitAliasesContextConfig config = clazz.getAnnotation(ImplicitAliasesContextConfig.class);
assertNotNull(config);
ImplicitAliasesContextConfig synthesizedConfig = synthesizeAnnotation(config);
assertThat(synthesizedConfig, instanceOf(SynthesizedAnnotation.class));
assertNotSame(config, synthesizedConfig);
assertEquals("value: ", expected, synthesizedConfig.value());
assertEquals("location1: ", expected, synthesizedConfig.location1());
assertEquals("xmlFile: ", expected, synthesizedConfig.xmlFile());
assertEquals("groovyScript: ", expected, synthesizedConfig.groovyScript());
}
@Test
public void synthesizeAnnotationWithImplicitAliasesWithMissingDefaultValues() throws Exception {
Class<?> clazz = ImplicitAliasesWithMissingDefaultValuesContextConfigClass.class;
Class<ImplicitAliasesWithMissingDefaultValuesContextConfig> annotationType = ImplicitAliasesWithMissingDefaultValuesContextConfig.class;
ImplicitAliasesWithMissingDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
assertNotNull(config);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Misconfigured aliases:"));
exception.expectMessage(containsString("attribute [location1] in annotation [" + annotationType.getName() + "]"));
exception.expectMessage(containsString("attribute [location2] in annotation [" + annotationType.getName() + "]"));
exception.expectMessage(endsWith("must declare default values."));
synthesizeAnnotation(config, clazz);
}
@Test
public void synthesizeAnnotationWithImplicitAliasesWithDifferentDefaultValues() throws Exception {
Class<?> clazz = ImplicitAliasesWithDifferentDefaultValuesContextConfigClass.class;
Class<ImplicitAliasesWithDifferentDefaultValuesContextConfig> annotationType = ImplicitAliasesWithDifferentDefaultValuesContextConfig.class;
ImplicitAliasesWithDifferentDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
assertNotNull(config);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("Misconfigured aliases:"));
exception.expectMessage(containsString("attribute [location1] in annotation [" + annotationType.getName() + "]"));
exception.expectMessage(containsString("attribute [location2] in annotation [" + annotationType.getName() + "]"));
exception.expectMessage(endsWith("must declare the same default value."));
synthesizeAnnotation(config, clazz);
}
@Test
public void synthesizeAnnotationWithImplicitAliasesWithDuplicateValues() throws Exception {
Class<?> clazz = ImplicitAliasesWithDuplicateValuesContextConfigClass.class;
Class<ImplicitAliasesWithDuplicateValuesContextConfig> annotationType = ImplicitAliasesWithDuplicateValuesContextConfig.class;
ImplicitAliasesWithDuplicateValuesContextConfig config = clazz.getAnnotation(annotationType);
assertNotNull(config);
ImplicitAliasesWithDuplicateValuesContextConfig synthesizedConfig = synthesizeAnnotation(config, clazz);
assertNotNull(synthesizedConfig);
exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(startsWith("In annotation"));
exception.expectMessage(containsString(annotationType.getName()));
exception.expectMessage(containsString("declared on class"));
exception.expectMessage(containsString(clazz.getName()));
exception.expectMessage(containsString("and synthesized from"));
exception.expectMessage(either(containsString("attribute 'location1' and its alias 'location2'")).or(
containsString("attribute 'location2' and its alias 'location1'")));
exception.expectMessage(either(containsString("are present with values of [1] and [2]")).or(
containsString("are present with values of [2] and [1]")));
exception.expectMessage(endsWith("but only one is permitted."));
synthesizedConfig.location1();
}
@Test
public void synthesizeAnnotationFromMapWithoutAttributeAliases() throws Exception {
Component component = WebController.class.getAnnotation(Component.class);
assertNotNull(component);
Map<String, Object> map = new HashMap<String, Object>();
map.put(VALUE, "webController");
Map<String, Object> map = Collections.singletonMap(VALUE, "webController");
Component synthesizedComponent = synthesizeAnnotation(map, Component.class, WebController.class);
assertNotNull(synthesizedComponent);
@ -979,14 +1092,35 @@ public class AnnotationUtilsTests { @@ -979,14 +1092,35 @@ public class AnnotationUtilsTests {
@Test
public void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
map.put("location", "test.xml");
Map<String, Object> map = Collections.singletonMap("location", "test.xml");
ContextConfig contextConfig = synthesizeAnnotation(map, ContextConfig.class, null);
assertNotNull(contextConfig);
assertEquals("value: ", "test.xml", contextConfig.value());
assertEquals("location: ", "test.xml", contextConfig.location());
}
@Test
public void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception {
assertAnnotationSynthesisFromMapWithImplicitAliases("value");
assertAnnotationSynthesisFromMapWithImplicitAliases("location1");
assertAnnotationSynthesisFromMapWithImplicitAliases("location2");
assertAnnotationSynthesisFromMapWithImplicitAliases("location3");
assertAnnotationSynthesisFromMapWithImplicitAliases("xmlFile");
assertAnnotationSynthesisFromMapWithImplicitAliases("groovyScript");
}
private void assertAnnotationSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) throws Exception {
Map<String, Object> map = Collections.singletonMap(attributeNameAndValue, attributeNameAndValue);
ImplicitAliasesContextConfig config = synthesizeAnnotation(map, ImplicitAliasesContextConfig.class, null);
assertNotNull(config);
assertEquals("value: ", attributeNameAndValue, config.value());
assertEquals("location1: ", attributeNameAndValue, config.location1());
assertEquals("location2: ", attributeNameAndValue, config.location2());
assertEquals("location3: ", attributeNameAndValue, config.location3());
assertEquals("xmlFile: ", attributeNameAndValue, config.xmlFile());
assertEquals("groovyScript: ", attributeNameAndValue, config.groovyScript());
}
@Test
public void synthesizeAnnotationFromMapWithMissingAttributeValue() throws Exception {
assertMissingTextAttribute(Collections.emptyMap());
@ -994,8 +1128,7 @@ public class AnnotationUtilsTests { @@ -994,8 +1128,7 @@ public class AnnotationUtilsTests {
@Test
public void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
map.put("text", null);
Map<String, Object> map = Collections.singletonMap("text", null);
assertTrue(map.containsKey("text"));
assertMissingTextAttribute(map);
}
@ -1010,8 +1143,7 @@ public class AnnotationUtilsTests { @@ -1010,8 +1143,7 @@ public class AnnotationUtilsTests {
@Test
public void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
map.put(VALUE, 42L);
Map<String, Object> map = Collections.singletonMap(VALUE, 42L);
exception.expect(IllegalArgumentException.class);
exception.expectMessage(startsWith("Attributes map"));
@ -1183,7 +1315,7 @@ public class AnnotationUtilsTests { @@ -1183,7 +1315,7 @@ public class AnnotationUtilsTests {
@Test
public void synthesizeAnnotationWithAttributeAliasesInNestedAnnotations() throws Exception {
List<String> expectedLocations = Arrays.asList("A", "B");
List<String> expectedLocations = asList("A", "B");
Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class);
assertNotNull(hierarchy);
@ -1194,18 +1326,18 @@ public class AnnotationUtilsTests { @@ -1194,18 +1326,18 @@ public class AnnotationUtilsTests {
ContextConfig[] configs = synthesizedHierarchy.value();
assertNotNull(configs);
assertTrue("nested annotations must be synthesized",
Arrays.stream(configs).allMatch(c -> c instanceof SynthesizedAnnotation));
stream(configs).allMatch(c -> c instanceof SynthesizedAnnotation));
List<String> locations = Arrays.stream(configs).map(ContextConfig::location).collect(toList());
List<String> locations = stream(configs).map(ContextConfig::location).collect(toList());
assertThat(locations, is(expectedLocations));
List<String> values = Arrays.stream(configs).map(ContextConfig::value).collect(toList());
List<String> values = stream(configs).map(ContextConfig::value).collect(toList());
assertThat(values, is(expectedLocations));
}
@Test
public void synthesizeAnnotationWithArrayOfAnnotations() throws Exception {
List<String> expectedLocations = Arrays.asList("A", "B");
List<String> expectedLocations = asList("A", "B");
Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class);
assertNotNull(hierarchy);
@ -1216,7 +1348,7 @@ public class AnnotationUtilsTests { @@ -1216,7 +1348,7 @@ public class AnnotationUtilsTests {
assertNotNull(contextConfig);
ContextConfig[] configs = synthesizedHierarchy.value();
List<String> locations = Arrays.stream(configs).map(ContextConfig::location).collect(toList());
List<String> locations = stream(configs).map(ContextConfig::location).collect(toList());
assertThat(locations, is(expectedLocations));
// Alter array returned from synthesized annotation
@ -1224,7 +1356,7 @@ public class AnnotationUtilsTests { @@ -1224,7 +1356,7 @@ public class AnnotationUtilsTests {
// Re-retrieve the array from the synthesized annotation
configs = synthesizedHierarchy.value();
List<String> values = Arrays.stream(configs).map(ContextConfig::value).collect(toList());
List<String> values = stream(configs).map(ContextConfig::value).collect(toList());
assertThat(values, is(expectedLocations));
}
@ -1595,6 +1727,8 @@ public class AnnotationUtilsTests { @@ -1595,6 +1727,8 @@ public class AnnotationUtilsTests {
@AliasFor("value")
String location() default "";
Class<?> klass() default Object.class;
}
@Retention(RetentionPolicy.RUNTIME)
@ -1770,6 +1904,109 @@ public class AnnotationUtilsTests { @@ -1770,6 +1904,109 @@ public class AnnotationUtilsTests {
String xmlConfigFile();
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface ImplicitAliasesContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String xmlFile() default "";
@AliasFor(annotation = ContextConfig.class, value = "location")
String groovyScript() default "";
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String value() default "";
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location1() default "";
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location2() default "";
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location3() default "";
@AliasFor(annotation = ContextConfig.class, attribute = "klass")
Class<?> configClass() default Object.class;
String nonAliasedAttribute() default "";
}
// Attribute value intentionally matches attribute name:
@ImplicitAliasesContextConfig(groovyScript = "groovyScript")
static class GroovyImplicitAliasesContextConfigClass {
}
// Attribute value intentionally matches attribute name:
@ImplicitAliasesContextConfig(xmlFile = "xmlFile")
static class XmlImplicitAliasesContextConfigClass {
}
// Attribute value intentionally matches attribute name:
@ImplicitAliasesContextConfig("value")
static class ValueImplicitAliasesContextConfigClass {
}
// Attribute value intentionally matches attribute name:
@ImplicitAliasesContextConfig(location1 = "location1")
static class Location1ImplicitAliasesContextConfigClass {
}
// Attribute value intentionally matches attribute name:
@ImplicitAliasesContextConfig(location2 = "location2")
static class Location2ImplicitAliasesContextConfigClass {
}
// Attribute value intentionally matches attribute name:
@ImplicitAliasesContextConfig(location3 = "location3")
static class Location3ImplicitAliasesContextConfigClass {
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface ImplicitAliasesWithMissingDefaultValuesContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location1();
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location2();
}
@ImplicitAliasesWithMissingDefaultValuesContextConfig(location1 = "1", location2 = "2")
static class ImplicitAliasesWithMissingDefaultValuesContextConfigClass {
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface ImplicitAliasesWithDifferentDefaultValuesContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location1() default "foo";
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location2() default "bar";
}
@ImplicitAliasesWithDifferentDefaultValuesContextConfig(location1 = "1", location2 = "2")
static class ImplicitAliasesWithDifferentDefaultValuesContextConfigClass {
}
@ContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface ImplicitAliasesWithDuplicateValuesContextConfig {
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location1() default "";
@AliasFor(annotation = ContextConfig.class, attribute = "location")
String location2() default "";
}
@ImplicitAliasesWithDuplicateValuesContextConfig(location1 = "1", location2 = "2")
static class ImplicitAliasesWithDuplicateValuesContextConfigClass {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {

34
spring-core/src/test/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractorTests.java

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
/**
* Unit tests for {@link DefaultAnnotationAttributeExtractor}.
*
* @author Sam Brannen
* @since 4.2.1
*/
public class DefaultAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase {
@Override
protected AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType) {
return new DefaultAnnotationAttributeExtractor(clazz.getAnnotation(annotationType), clazz);
}
}

127
spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java

@ -0,0 +1,127 @@ @@ -0,0 +1,127 @@
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link MapAnnotationAttributeExtractor}.
*
* @author Sam Brannen
* @since 4.2.1
*/
public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase {
@Before
public void clearCachesBeforeTests() {
AnnotationUtilsTests.clearCaches();
}
@Test
@SuppressWarnings("serial")
public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() {
Map<String, Object> attributes = new HashMap<String, Object>();
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
put("groovyScript", "");
put("xmlFile", "");
put("value", "");
put("location1", "");
put("location2", "");
put("location3", "");
put("nonAliasedAttribute", "");
put("configClass", Object.class);
}};
assertEnrichAndValidateAttributes(attributes, expectedAttributes);
}
@Test
@SuppressWarnings("serial")
public void enrichAndValidateAttributesWithImplicitAliases() {
Map<String, Object> attributes = new HashMap<String, Object>() {{
put("groovyScript", "groovy!");
}};
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
put("groovyScript", "groovy!");
put("xmlFile", "groovy!");
put("value", "groovy!");
put("location1", "groovy!");
put("location2", "groovy!");
put("location3", "groovy!");
put("nonAliasedAttribute", "");
put("configClass", Object.class);
}};
assertEnrichAndValidateAttributes(attributes, expectedAttributes);
}
@SuppressWarnings("unchecked")
private void assertEnrichAndValidateAttributes(Map<String, Object> sourceAttributes, Map<String, Object> expected) {
Class<? extends Annotation> annotationType = ImplicitAliasesContextConfig.class;
// Since the ordering of attribute methods returned by the JVM is
// non-deterministic, we have to rig the attributeAliasesCache in AnnotationUtils
// so that the tests consistently fail in case enrichAndValidateAttributes() is
// buggy.
//
// Otherwise, these tests would intermittently pass even for an invalid
// implementation.
Map<Class<? extends Annotation>, MultiValueMap<String, String>> attributeAliasesCache =
(Map<Class<? extends Annotation>, MultiValueMap<String, String>>) AnnotationUtilsTests.getCache("attributeAliasesCache");
// Declare aliases in an order that will cause enrichAndValidateAttributes() to
// fail unless it considers all aliases in the set of implicit aliases.
MultiValueMap<String, String> aliases = new LinkedMultiValueMap<String, String>();
aliases.put("xmlFile", Arrays.asList("value", "groovyScript", "location1", "location2", "location3"));
aliases.put("groovyScript", Arrays.asList("value", "xmlFile", "location1", "location2", "location3"));
aliases.put("value", Arrays.asList("xmlFile", "groovyScript", "location1", "location2", "location3"));
aliases.put("location1", Arrays.asList("xmlFile", "groovyScript", "value", "location2", "location3"));
aliases.put("location2", Arrays.asList("xmlFile", "groovyScript", "value", "location1", "location3"));
aliases.put("location3", Arrays.asList("xmlFile", "groovyScript", "value", "location1", "location2"));
attributeAliasesCache.put(annotationType, aliases);
MapAnnotationAttributeExtractor extractor = new MapAnnotationAttributeExtractor(sourceAttributes, annotationType, null);
Map<String, Object> enriched = extractor.getSource();
assertEquals("attribute map size", expected.size(), enriched.size());
expected.keySet().stream().forEach( attr ->
assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expected.get(attr))));
}
@Override
protected AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType) {
Map<String, Object> attributes = Collections.singletonMap(expected, expected);
return new MapAnnotationAttributeExtractor(attributes, annotationType, clazz);
}
}
Loading…
Cancel
Save