Support implicit attribute aliases with @AliasFor

Spring Framework 4.2 introduced support for aliases between annotation
attributes that fall into the following two categories.

1) Alias pairs: two attributes in the same annotation that use
   @AliasFor to declare that they are explicit aliases for each other.
2) Meta-annotation attribute overrides: an attribute in one annotation
   uses @AliasFor to declare that it is an explicit override of an
   attribute in a meta-annotation.

However, the existing functionality fails to support the case where two
attributes in the same annotation both use @AliasFor to declare that
they are both explicit overrides of the same attribute in the same
meta-annotation. In such scenarios, one would intuitively assume that
two such attributes would be treated as "implicit" aliases for each
other, analogous to the existing support for explicit alias pairs.
Furthermore, an annotation may potentially declare multiple aliases
that are effectively a set of implicit aliases for each other.

This commit introduces support for implicit aliases configured via
@AliasFor through an extensive overhaul of the support for alias
lookups, validation, etc. Specifically, this commit includes the
following.

- Introduced isAnnotationMetaPresent() in AnnotationUtils.

- Introduced private AliasDescriptor class in AnnotationUtils in order
  to encapsulate the parsing, validation, and comparison of both
  explicit and implicit aliases configured via @AliasFor.

- Switched from single values for alias names to lists of alias names.

- Renamed getAliasedAttributeName() to getAliasedAttributeNames() in
  AnnotationUtils.

- Converted alias map to contain lists of aliases in AnnotationUtils.

- Refactored the following to support multiple implicit aliases:
  getRequiredAttributeWithAlias() in AnnotationAttributes,
  AbstractAliasAwareAnnotationAttributeExtractor,
  MapAnnotationAttributeExtractor, MergedAnnotationAttributesProcessor
  in AnnotatedElementUtils, and postProcessAnnotationAttributes() in
  AnnotationUtils.

- Introduced numerous tests for implicit alias support, including
  AbstractAliasAwareAnnotationAttributeExtractorTestCase,
  DefaultAnnotationAttributeExtractorTests, and
  MapAnnotationAttributeExtractorTests.

- Updated Javadoc in @AliasFor regarding implicit aliases and in
  AnnotationUtils regarding "meta-present".

Issue: SPR-13345
This commit is contained in:
Sam Brannen 2015-08-02 15:35:15 +02:00
parent ff9fb9aa88
commit d40a35ba5c
12 changed files with 1368 additions and 378 deletions

View File

@ -19,6 +19,7 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -44,7 +45,7 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
private final S source; private final S source;
private final Map<String, String> attributeAliasMap; private final Map<String, List<String>> attributeAliasMap;
/** /**
@ -83,13 +84,15 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
@Override @Override
public final Object getAttributeValue(Method attributeMethod) { public final Object getAttributeValue(Method attributeMethod) {
String attributeName = attributeMethod.getName(); final String attributeName = attributeMethod.getName();
Object attributeValue = getRawAttributeValue(attributeMethod); Object attributeValue = getRawAttributeValue(attributeMethod);
String aliasName = this.attributeAliasMap.get(attributeName); List<String> aliasNames = this.attributeAliasMap.get(attributeName);
if (aliasNames != null) {
final Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
for (String aliasName : aliasNames) {
if (aliasName != null) { if (aliasName != null) {
Object aliasValue = getRawAttributeValue(aliasName); Object aliasValue = getRawAttributeValue(aliasName);
Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) && if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) && !ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
@ -103,11 +106,13 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
} }
// If the user didn't declare the annotation with an explicit value, // If the user didn't declare the annotation with an explicit value,
// return the value of the alias. // use the value of the alias instead.
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) { if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
attributeValue = aliasValue; attributeValue = aliasValue;
} }
} }
}
}
return attributeValue; return attributeValue;
} }

View File

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

View File

@ -31,7 +31,6 @@ import org.springframework.core.BridgeMethodResolver;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/** /**
* General utility methods for finding annotations and meta-annotations on * General utility methods for finding annotations and meta-annotations on
@ -957,13 +956,21 @@ public class AnnotatedElementUtils {
for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) { for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {
String attributeName = attributeMethod.getName(); String attributeName = attributeMethod.getName();
String aliasedAttributeName = AnnotationUtils.getAliasedAttributeName(attributeMethod, List<String> aliases = AnnotationUtils.getAliasedAttributeNames(attributeMethod, targetAnnotationType);
targetAnnotationType);
// Explicit annotation attribute override declared via @AliasFor // Explicit annotation attribute override declared via @AliasFor
if (StringUtils.hasText(aliasedAttributeName) && attributes.containsKey(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); overrideAttribute(element, annotation, attributes, attributeName, aliasedAttributeName);
} }
}
// Implicit annotation attribute override based on convention // Implicit annotation attribute override based on convention
else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) { else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
overrideAttribute(element, annotation, attributes, attributeName, attributeName); overrideAttribute(element, annotation, attributes, attributeName, attributeName);

View File

@ -21,6 +21,7 @@ import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -422,12 +423,15 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
Assert.notNull(expectedType, "expectedType must not be null"); Assert.notNull(expectedType, "expectedType must not be null");
T attributeValue = getAttribute(attributeName, expectedType); 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) { List<String> aliasNames = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
if (aliasNames != null) {
for (String aliasName : aliasNames) {
T aliasValue = getAttribute(aliasName, expectedType);
boolean attributeEmpty = ObjectUtils.isEmpty(attributeValue);
boolean aliasEmpty = ObjectUtils.isEmpty(aliasValue);
if (!attributeEmpty && !aliasEmpty && !ObjectUtils.nullSafeEquals(attributeValue, aliasValue)) {
String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString()); String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString());
String msg = String.format("In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] " + 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.", "are present with values of [%s] and [%s], but only one is permitted.",
@ -436,11 +440,20 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
throw new AnnotationConfigurationException(msg); throw new AnnotationConfigurationException(msg);
} }
if (!attributeDeclared) { // 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; attributeValue = aliasValue;
} }
// Else: if we're not expecting an array, we can rely on the behavior of
assertAttributePresence(attributeName, aliasName, attributeValue); // ObjectUtils.isEmpty().
else if (attributeEmpty && !aliasEmpty) {
attributeValue = aliasValue;
}
}
assertAttributePresence(attributeName, aliasNames, attributeValue);
}
return attributeValue; return attributeValue;
} }
@ -473,11 +486,11 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
} }
} }
private void assertAttributePresence(String attributeName, String aliasName, Object attributeValue) { private void assertAttributePresence(String attributeName, List<String> aliases, Object attributeValue) {
if (attributeValue == null) { if (attributeValue == null) {
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(String.format(
"Neither attribute '%s' nor its alias '%s' was found in attributes for annotation [%s]", "Neither attribute '%s' nor one of its aliases %s was found in attributes for annotation [%s]",
attributeName, aliasName, this.displayName)); attributeName, aliases, this.displayName));
} }
} }

View File

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

View File

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

View File

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

View File

@ -323,16 +323,62 @@ public class AnnotatedElementUtilsTests {
} }
@Test @Test
public void getMergeAndSynthesizeAnnotationWithAliasedValueComposedAnnotation() { public void getMergedAnnotationAttributesWithImplicitAliasesInMetaAnnotationOnComposedAnnotation() {
Class<?> element = AliasedValueComposedContextConfigClass.class; 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); ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class);
assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), contextConfig); assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), contextConfig);
assertArrayEquals("locations", new String[] { "test.xml" }, contextConfig.locations()); assertArrayEquals("locations", new String[] { expected }, contextConfig.locations());
assertArrayEquals("value", new String[] { "test.xml" }, contextConfig.value()); assertArrayEquals("value", new String[] { expected }, contextConfig.value());
assertArrayEquals("classes", new Class<?>[0], contextConfig.classes());
// Verify contracts between utility methods: // 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 @Test
@ -517,11 +563,11 @@ public class AnnotatedElementUtilsTests {
@Test @Test
public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() { public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
String[] expected = new String[] { "com.example.app.test" };
Class<?> element = TestComponentScanClass.class; Class<?> element = TestComponentScanClass.class;
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class); AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
assertNotNull("Should find @ComponentScan on " + element, attributes); assertNotNull("Should find @ComponentScan on " + element, attributes);
assertArrayEquals("basePackages for " + element, new String[] { "com.example.app.test" }, assertArrayEquals("basePackages for " + element, expected, attributes.getStringArray("basePackages"));
attributes.getStringArray("basePackages"));
Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class); Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class);
assertNotNull(excludeFilters); assertNotNull(excludeFilters);
@ -530,6 +576,22 @@ public class AnnotatedElementUtilsTests {
assertEquals(asList("*Test", "*Tests"), patterns); 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 @Test
public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() {
final String[] EMPTY = new String[] {}; final String[] EMPTY = new String[] {};
@ -716,6 +778,28 @@ public class AnnotatedElementUtilsTests {
String[] locations(); 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 * Invalid because the configuration declares a value for 'value' and
* requires a value for the aliased 'locations'. So we likely end up with * requires a value for the aliased 'locations'. So we likely end up with
@ -762,6 +846,10 @@ public class AnnotatedElementUtilsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface ComponentScan { @interface ComponentScan {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {}; String[] basePackages() default {};
Filter[] excludeFilters() default {}; Filter[] excludeFilters() default {};
@ -928,6 +1016,22 @@ public class AnnotatedElementUtilsTests {
static class AliasedValueComposedContextConfigClass { 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") @InvalidAliasedComposedContextConfig(xmlConfigFiles = "test.xml")
static class InvalidAliasedComposedContextConfigClass { static class InvalidAliasedComposedContextConfigClass {
} }
@ -936,6 +1040,10 @@ public class AnnotatedElementUtilsTests {
static class AliasedComposedContextConfigAndTestPropSourceClass { static class AliasedComposedContextConfigAndTestPropSourceClass {
} }
@ComponentScan(value = "com.example.app.test", basePackages = "com.example.app.test")
static class ComponentScanWithBasePackagesAndValueAliasClass {
}
@TestComponentScan(packages = "com.example.app.test") @TestComponentScan(packages = "com.example.app.test")
static class TestComponentScanClass { static class TestComponentScanClass {
} }

View File

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

View File

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

View File

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

View File

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