diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java index a48481ca546..2b79039df53 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractor.java @@ -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 implements Anno private final S source; - private final Map attributeAliasMap; + private final Map> attributeAliasMap; /** @@ -83,29 +84,33 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor 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 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; + } + } } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java index 2ac61d61677..c116695245f 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java @@ -29,10 +29,10 @@ import java.lang.annotation.Target; * *

Usage Scenarios

*
    - *
  • Aliases within an annotation: within a single + *
  • Explicit aliases within an annotation: within a single * annotation, {@code @AliasFor} can be declared on a pair of attributes to * signal that they are interchangeable aliases for each other.
  • - *
  • Alias for attribute in meta-annotation: if the + *
  • Explicit alias for attribute in meta-annotation: 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; * 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.
  • + *
  • Implicit aliases within an annotation: 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 implicit + * aliases for each other, analogous to explicit aliases within an annotation.
  • *
* *

Usage Requirements

@@ -57,31 +62,44 @@ import java.lang.annotation.Target; * *

Implementation Requirements

*
    - *
  • Aliases within an annotation: + *
  • Explicit aliases within an annotation: *
      *
    1. 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 other attribute in the pair.
    2. + * {@code @AliasFor}, and either {@link #attribute} or {@link #value} must + * reference the other attribute in the pair. *
    3. Aliased attributes must declare the same return type.
    4. *
    5. Aliased attributes must declare a default value.
    6. *
    7. Aliased attributes must declare the same default value.
    8. - *
    9. The {@link #annotation} attribute should remain set to the default.
    10. + *
    11. {@link #annotation} should not be declared.
    12. *
    *
  • - *
  • Alias for attribute in meta-annotation: + *
  • Explicit alias for attribute in meta-annotation: *
      *
    1. 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.
    2. + * must be annotated with {@code @AliasFor}, and {@link #attribute} must + * reference the attribute in the meta-annotation. *
    3. Aliased attributes must declare the same return type.
    4. - *
    5. The {@link #annotation} must reference the meta-annotation.
    6. + *
    7. {@link #annotation} must reference the meta-annotation.
    8. + *
    9. The referenced meta-annotation must be meta-present on the + * annotation class that declares {@code @AliasFor}.
    10. + *
    + *
  • + *
  • Implicit aliases within an annotation: + *
      + *
    1. 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.
    2. + *
    3. Aliased attributes must declare the same return type.
    4. + *
    5. Aliased attributes must declare a default value.
    6. + *
    7. Aliased attributes must declare the same default value.
    8. + *
    9. {@link #annotation} must reference the meta-annotation.
    10. *
    11. The referenced meta-annotation must be meta-present on the * annotation class that declares {@code @AliasFor}.
    12. *
    *
  • *
* - *

Example: Aliases within an Annotation

+ *

Example: Explicit Aliases within an Annotation

*
 public @interface ContextConfiguration {
  *
  *    @AliasFor("locations")
@@ -93,7 +111,7 @@ import java.lang.annotation.Target;
  *    // ...
  * }
* - *

Example: Alias for Attribute in Meta-annotation

+ *

Example: Explicit Alias for Attribute in Meta-annotation

*
 @ContextConfiguration
  * public @interface MyTestConfig {
  *
@@ -101,6 +119,20 @@ import java.lang.annotation.Target;
  *    String[] xmlFiles();
  * }
* + *

Example: Implicit Aliases within an Annotation

+ *
 @ContextConfiguration
+ * public @interface MyTestConfig {
+ *
+ *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
+ *    String[] value() default {};
+ *
+ *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
+ *    String[] groovyScripts() default {};
+ *
+ *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
+ *    String[] xmlFiles() default {};
+ * }
+ * *

Spring Annotations Supporting Attribute Aliases

*

As of Spring Framework 4.2, several annotations within core Spring * have been updated to use {@code @AliasFor} to configure their internal diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index e4acbbcdf96..0b395161824 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -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 { for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) { String attributeName = attributeMethod.getName(); - String aliasedAttributeName = AnnotationUtils.getAliasedAttributeName(attributeMethod, - targetAnnotationType); + List 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); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java index 3623820acb8..395d2dba309 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -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 { 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 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 { } } - private void assertAttributePresence(String attributeName, String aliasName, Object attributeValue) { + private void assertAttributePresence(String attributeName, List 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)); } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 6ab73081ba1..3ca49bcbceb 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -67,7 +67,9 @@ import org.springframework.util.StringUtils; * *

An annotation is meta-present on an element if the annotation * is declared as a meta-annotation on some other annotation which is - * present on the element. + * present on the element. Annotation {@code A} is meta-present + * on another annotation if {@code A} is either directly present or + * meta-present on the other annotation. * *

Meta-annotation Support

*

Most {@code find*()} methods and some {@code get*()} methods in this @@ -123,11 +125,14 @@ public abstract class AnnotationUtils { private static final Map, Boolean> annotatedInterfaceCache = new ConcurrentReferenceHashMap, Boolean>(256); + private static final Map metaPresentCache = + new ConcurrentReferenceHashMap(256); + private static final Map, Boolean> synthesizableCache = new ConcurrentReferenceHashMap, Boolean>(256); - private static final Map, Map> attributeAliasesCache = - new ConcurrentReferenceHashMap, Map>(256); + private static final Map, Map>> attributeAliasesCache = + new ConcurrentReferenceHashMap, Map>>(256); private static final Map, List> attributeMethodsCache = new ConcurrentReferenceHashMap, List>(256); @@ -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 findAnnotation(Class clazz, Class 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 findAnnotation(Class clazz, Class 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 { findAnnotationCache.put(cacheKey, result); } } - return synthesizeAnnotation(result, clazz); + return (synthesize ? synthesizeAnnotation(result, clazz) : result); } /** @@ -833,6 +852,30 @@ public abstract class AnnotationUtils { return (clazz.isAnnotationPresent(annotationType) && !isAnnotationDeclaredLocally(annotationType, clazz)); } + /** + * Determine if an annotation of type {@code metaAnnotationType} is + * meta-present 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 annotationType, + Class 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 { } /** - * 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. *

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

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

For implicit 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)}. *

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 getAttributeAliasMap(Class annotationType) { + static Map> getAttributeAliasMap(Class annotationType) { if (annotationType == null) { return Collections.emptyMap(); } - Map map = attributeAliasesCache.get(annotationType); + Map> map = attributeAliasesCache.get(annotationType); if (map != null) { return map; } - map = new HashMap(); + map = new HashMap>(); for (Method attribute : getAttributeMethods(annotationType)) { - String attributeName = attribute.getName(); - String aliasedAttributeName = getAliasedAttributeName(attribute); - if (aliasedAttributeName != null) { - map.put(attributeName, aliasedAttributeName); + List aliasNames = getAliasedAttributeNames(attribute); + if (!aliasNames.isEmpty()) { + map.put(attribute.getName(), aliasNames); } } @@ -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 { } /** - * Get the name of the aliased attribute configured via - * {@link AliasFor @AliasFor} on the supplied annotation {@code attribute}. - *

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

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 empty * @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) null); + static List getAliasedAttributeNames(Method attribute) { + return getAliasedAttributeNames(attribute, (Class) 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}. + *

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 - * within the same annotation - * @return the name of the aliased attribute, or {@code null} if not found + * within the same annotation as the supplied attribute + * @return the names of the aliased attributes; never {@code null}, though + * potentially empty * @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 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 getAliasedAttributeNames(Method attribute, Class 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 sourceAnnotationType = (Class) declaringClass; - Class 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 aliases = new ArrayList(); + 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}. - *

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 { * 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 { * 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 { Class annotationType = attributes.annotationType(); + // Track which attribute values have already been replaced so that we can short + // circuit the search algorithms. + Set valuesAlreadyReplaced = new HashSet(); + // Validate @AliasFor configuration - Map aliasMap = getAttributeAliasMap(annotationType); - Set validated = new HashSet(); + Map> 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 { } } + /** + * {@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 sourceAnnotationType; + + private final String sourceAttributeName; + + private final Class aliasedAnnotationType; + + private final String aliasedAttributeName; + + private final boolean isAliasPair; + + + /** + * Create a new {@code AliasDescriptor} from 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) 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. + *

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 targetAnnotationType) { + return targetAnnotationType.equals(this.aliasedAnnotationType); + } + + /** + * Determine if this descriptor is logically equal to the supplied + * object. + *

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

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(); + } + } + } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java index 0f5591c16b7..ddf9976ac62 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java @@ -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 Map originalAttributes, Class annotationType) { Map attributes = new HashMap(originalAttributes); - Map attributeAliasMap = getAttributeAliasMap(annotationType); + Map> 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 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) { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractorTestCase.java b/spring-core/src/test/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractorTestCase.java new file mode 100644 index 00000000000..4eb77f8a386 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AbstractAliasAwareAnnotationAttributeExtractorTestCase.java @@ -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 annotationType); + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 069633888b6..feccac0c1da 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -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 { @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 { 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 { 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 { @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 { 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 { 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 { } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java index 880b18213a7..cb7af8502ff 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java @@ -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.*; */ 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 { @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 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 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 { @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 { 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 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 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 { assertArrayEquals(EMPTY, getAliasedClassArray("value")); } + @Test + public void getAliasedClassArrayWithImplicitAliases() { + final Class[] INPUT = new Class[] { String.class }; + final Class[] EMPTY = new Class[0]; + final List 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 { 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 { 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}. */ diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index c9721fb7397..77e4e69bbc8 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -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; 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.*; */ 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 { @Test public void findAnnotationDeclaringClassForTypesWithSingleCandidateType() { // no class-level annotation - List> transactionalCandidateList = Arrays.> asList(Transactional.class); + List> transactionalCandidateList = asList(Transactional.class); assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedInterface.class)); assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedClass.class)); @@ -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> orderCandidateList = Arrays.> asList(Order.class); + List> orderCandidateList = asList(Order.class); assertEquals(NonInheritedAnnotationInterface.class, findAnnotationDeclaringClassForTypes(orderCandidateList, NonInheritedAnnotationInterface.class)); assertNull(findAnnotationDeclaringClassForTypes(orderCandidateList, SubNonInheritedAnnotationInterface.class)); @@ -335,7 +357,7 @@ public class AnnotationUtilsTests { @Test public void findAnnotationDeclaringClassForTypesWithMultipleCandidateTypes() { - List> candidates = Arrays.> asList(Transactional.class, Order.class); + List> candidates = asList(Transactional.class, Order.class); // no class-level annotation assertNull(findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedInterface.class)); @@ -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 { Set annotations = getRepeatableAnnotations(method, MyRepeatable.class, MyRepeatableContainer.class); assertNotNull(annotations); List 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 expectedLocations = Arrays.asList("A", "B"); + final List expectedLocations = asList("A", "B"); Set annotations = getRepeatableAnnotations(ConfigHierarchyTestCase.class, ContextConfig.class, null); assertNotNull(annotations); @@ -556,8 +578,8 @@ public class AnnotationUtilsTests { @Test public void getRepeatableAnnotationsDeclaredOnClass() { - final List expectedValuesJava = Arrays.asList("A", "B", "C"); - final List expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1"); + final List expectedValuesJava = asList("A", "B", "C"); + final List expectedValuesSpring = asList("A", "B", "C", "meta1"); // Java 8 MyRepeatable[] array = MyRepeatableClass.class.getAnnotationsByType(MyRepeatable.class); @@ -581,8 +603,8 @@ public class AnnotationUtilsTests { @Test public void getRepeatableAnnotationsDeclaredOnSuperclass() { final Class clazz = SubMyRepeatableClass.class; - final List expectedValuesJava = Arrays.asList("A", "B", "C"); - final List expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1"); + final List expectedValuesJava = asList("A", "B", "C"); + final List expectedValuesSpring = asList("A", "B", "C", "meta1"); // Java 8 MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class); @@ -606,8 +628,8 @@ public class AnnotationUtilsTests { @Test public void getRepeatableAnnotationsDeclaredOnClassAndSuperclass() { final Class clazz = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class; - final List expectedValuesJava = Arrays.asList("X", "Y", "Z"); - final List expectedValuesSpring = Arrays.asList("X", "Y", "Z", "meta2"); + final List expectedValuesJava = asList("X", "Y", "Z"); + final List expectedValuesSpring = asList("X", "Y", "Z", "meta2"); // Java 8 MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class); @@ -631,8 +653,8 @@ public class AnnotationUtilsTests { @Test public void getRepeatableAnnotationsDeclaredOnMultipleSuperclasses() { final Class clazz = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class; - final List expectedValuesJava = Arrays.asList("X", "Y", "Z"); - final List expectedValuesSpring = Arrays.asList("X", "Y", "Z", "meta2"); + final List expectedValuesJava = asList("X", "Y", "Z"); + final List expectedValuesSpring = asList("X", "Y", "Z", "meta2"); // Java 8 MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class); @@ -655,8 +677,8 @@ public class AnnotationUtilsTests { @Test public void getDeclaredRepeatableAnnotationsDeclaredOnClass() { - final List expectedValuesJava = Arrays.asList("A", "B", "C"); - final List expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1"); + final List expectedValuesJava = asList("A", "B", "C"); + final List expectedValuesSpring = asList("A", "B", "C", "meta1"); // Java 8 MyRepeatable[] array = MyRepeatableClass.class.getDeclaredAnnotationsByType(MyRepeatable.class); @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 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 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 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 map = new HashMap(); - map.put(VALUE, "webController"); + Map map = Collections.singletonMap(VALUE, "webController"); Component synthesizedComponent = synthesizeAnnotation(map, Component.class, WebController.class); assertNotNull(synthesizedComponent); @@ -979,14 +1092,35 @@ public class AnnotationUtilsTests { @Test public void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() throws Exception { - Map map = new HashMap(); - map.put("location", "test.xml"); + Map 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 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 { @Test public void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception { - Map map = new HashMap(); - map.put("text", null); + Map map = Collections.singletonMap("text", null); assertTrue(map.containsKey("text")); assertMissingTextAttribute(map); } @@ -1010,8 +1143,7 @@ public class AnnotationUtilsTests { @Test public void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() throws Exception { - Map map = new HashMap(); - map.put(VALUE, 42L); + Map map = Collections.singletonMap(VALUE, 42L); exception.expect(IllegalArgumentException.class); exception.expectMessage(startsWith("Attributes map")); @@ -1183,7 +1315,7 @@ public class AnnotationUtilsTests { @Test public void synthesizeAnnotationWithAttributeAliasesInNestedAnnotations() throws Exception { - List expectedLocations = Arrays.asList("A", "B"); + List expectedLocations = asList("A", "B"); Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class); assertNotNull(hierarchy); @@ -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 locations = Arrays.stream(configs).map(ContextConfig::location).collect(toList()); + List locations = stream(configs).map(ContextConfig::location).collect(toList()); assertThat(locations, is(expectedLocations)); - List values = Arrays.stream(configs).map(ContextConfig::value).collect(toList()); + List values = stream(configs).map(ContextConfig::value).collect(toList()); assertThat(values, is(expectedLocations)); } @Test public void synthesizeAnnotationWithArrayOfAnnotations() throws Exception { - List expectedLocations = Arrays.asList("A", "B"); + List expectedLocations = asList("A", "B"); Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class); assertNotNull(hierarchy); @@ -1216,7 +1348,7 @@ public class AnnotationUtilsTests { assertNotNull(contextConfig); ContextConfig[] configs = synthesizedHierarchy.value(); - List locations = Arrays.stream(configs).map(ContextConfig::location).collect(toList()); + List 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 { // Re-retrieve the array from the synthesized annotation configs = synthesizedHierarchy.value(); - List values = Arrays.stream(configs).map(ContextConfig::value).collect(toList()); + List values = stream(configs).map(ContextConfig::value).collect(toList()); assertThat(values, is(expectedLocations)); } @@ -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 { 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 { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractorTests.java b/spring-core/src/test/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractorTests.java new file mode 100644 index 00000000000..1a89e3fa40c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/DefaultAnnotationAttributeExtractorTests.java @@ -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 annotationType) { + return new DefaultAnnotationAttributeExtractor(clazz.getAnnotation(annotationType), clazz); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java new file mode 100644 index 00000000000..a38115272e5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java @@ -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 attributes = new HashMap(); + Map expectedAttributes = new HashMap() {{ + 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 attributes = new HashMap() {{ + put("groovyScript", "groovy!"); + }}; + + Map expectedAttributes = new HashMap() {{ + 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 sourceAttributes, Map expected) { + Class 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, MultiValueMap> attributeAliasesCache = + (Map, MultiValueMap>) 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 aliases = new LinkedMultiValueMap(); + 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 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 annotationType) { + Map attributes = Collections.singletonMap(expected, expected); + return new MapAnnotationAttributeExtractor(attributes, annotationType, clazz); + } + +}