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:
parent
ff9fb9aa88
commit
d40a35ba5c
|
@ -19,6 +19,7 @@ package org.springframework.core.annotation;
|
|||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -44,7 +45,7 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
|
|||
|
||||
private final S source;
|
||||
|
||||
private final Map<String, String> attributeAliasMap;
|
||||
private final Map<String, List<String>> attributeAliasMap;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -83,29 +84,33 @@ abstract class AbstractAliasAwareAnnotationAttributeExtractor<S> implements Anno
|
|||
|
||||
@Override
|
||||
public final Object getAttributeValue(Method attributeMethod) {
|
||||
String attributeName = attributeMethod.getName();
|
||||
final String attributeName = attributeMethod.getName();
|
||||
Object attributeValue = getRawAttributeValue(attributeMethod);
|
||||
|
||||
String aliasName = this.attributeAliasMap.get(attributeName);
|
||||
if (aliasName != null) {
|
||||
Object aliasValue = getRawAttributeValue(aliasName);
|
||||
Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
|
||||
List<String> aliasNames = this.attributeAliasMap.get(attributeName);
|
||||
if (aliasNames != null) {
|
||||
final Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);
|
||||
for (String aliasName : aliasNames) {
|
||||
if (aliasName != null) {
|
||||
Object aliasValue = getRawAttributeValue(aliasName);
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
|
||||
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
|
||||
!ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
|
||||
String elementName = (getAnnotatedElement() != null ? getAnnotatedElement().toString() : "unknown element");
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " +
|
||||
"alias '%s' are present with values of [%s] and [%s], but only one is permitted.",
|
||||
getAnnotationType().getName(), elementName, getSource(), attributeName, aliasName,
|
||||
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)));
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
|
||||
!ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
|
||||
!ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
|
||||
String elementName = (getAnnotatedElement() != null ? getAnnotatedElement().toString() : "unknown element");
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"In annotation [%s] declared on %s and synthesized from [%s], attribute '%s' and its " +
|
||||
"alias '%s' are present with values of [%s] and [%s], but only one is permitted.",
|
||||
getAnnotationType().getName(), elementName, getSource(), attributeName, aliasName,
|
||||
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue)));
|
||||
}
|
||||
|
||||
// If the user didn't declare the annotation with an explicit value,
|
||||
// return the value of the alias.
|
||||
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
|
||||
attributeValue = aliasValue;
|
||||
// If the user didn't declare the annotation with an explicit value,
|
||||
// use the value of the alias instead.
|
||||
if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
|
||||
attributeValue = aliasValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,10 +29,10 @@ import java.lang.annotation.Target;
|
|||
*
|
||||
* <h3>Usage Scenarios</h3>
|
||||
* <ul>
|
||||
* <li><strong>Aliases within an annotation</strong>: within a single
|
||||
* <li><strong>Explicit aliases within an annotation</strong>: within a single
|
||||
* annotation, {@code @AliasFor} can be declared on a pair of attributes to
|
||||
* signal that they are interchangeable aliases for each other.</li>
|
||||
* <li><strong>Alias for attribute in meta-annotation</strong>: if the
|
||||
* <li><strong>Explicit alias for attribute in meta-annotation</strong>: if the
|
||||
* {@link #annotation} attribute of {@code @AliasFor} is set to a different
|
||||
* annotation than the one that declares it, the {@link #attribute} is
|
||||
* interpreted as an alias for an attribute in a meta-annotation (i.e., an
|
||||
|
@ -40,6 +40,11 @@ import java.lang.annotation.Target;
|
|||
* control over exactly which attributes are overridden within an annotation
|
||||
* hierarchy. In fact, with {@code @AliasFor} it is even possible to declare
|
||||
* an alias for the {@code value} attribute of a meta-annotation.</li>
|
||||
* <li><strong>Implicit aliases within an annotation</strong>: if one or
|
||||
* more attributes within an annotation are declared as explicit
|
||||
* meta-annotation attribute overrides for the same attribute in the
|
||||
* meta-annotation, those attributes will be treated as a set of <em>implicit</em>
|
||||
* aliases for each other, analogous to explicit aliases within an annotation.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Usage Requirements</h3>
|
||||
|
@ -57,31 +62,44 @@ import java.lang.annotation.Target;
|
|||
*
|
||||
* <h3>Implementation Requirements</h3>
|
||||
* <ul>
|
||||
* <li><strong>Aliases within an annotation</strong>:
|
||||
* <li><strong>Explicit aliases within an annotation</strong>:
|
||||
* <ol>
|
||||
* <li>Each attribute that makes up an aliased pair must be annotated with
|
||||
* {@code @AliasFor}, and either the {@link #attribute} or the {@link #value}
|
||||
* attribute must reference the <em>other</em> attribute in the pair.</li>
|
||||
* {@code @AliasFor}, and either {@link #attribute} or {@link #value} must
|
||||
* reference the <em>other</em> attribute in the pair.</li>
|
||||
* <li>Aliased attributes must declare the same return type.</li>
|
||||
* <li>Aliased attributes must declare a default value.</li>
|
||||
* <li>Aliased attributes must declare the same default value.</li>
|
||||
* <li>The {@link #annotation} attribute should remain set to the default.</li>
|
||||
* <li>{@link #annotation} should not be declared.</li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* <li><strong>Alias for attribute in meta-annotation</strong>:
|
||||
* <li><strong>Explicit alias for attribute in meta-annotation</strong>:
|
||||
* <ol>
|
||||
* <li>The attribute that is an alias for an attribute in a meta-annotation
|
||||
* must be annotated with {@code @AliasFor}, and the {@link #attribute} must
|
||||
* reference the aliased attribute in the meta-annotation.</li>
|
||||
* must be annotated with {@code @AliasFor}, and {@link #attribute} must
|
||||
* reference the attribute in the meta-annotation.</li>
|
||||
* <li>Aliased attributes must declare the same return type.</li>
|
||||
* <li>The {@link #annotation} must reference the meta-annotation.</li>
|
||||
* <li>{@link #annotation} must reference the meta-annotation.</li>
|
||||
* <li>The referenced meta-annotation must be <em>meta-present</em> on the
|
||||
* annotation class that declares {@code @AliasFor}.</li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* <li><strong>Implicit aliases within an annotation</strong>:
|
||||
* <ol>
|
||||
* <li>Each attribute that belongs to the set of implicit aliases must be
|
||||
* annotated with {@code @AliasFor}, and {@link #attribute} must reference
|
||||
* the same attribute in the same meta-annotation.</li>
|
||||
* <li>Aliased attributes must declare the same return type.</li>
|
||||
* <li>Aliased attributes must declare a default value.</li>
|
||||
* <li>Aliased attributes must declare the same default value.</li>
|
||||
* <li>{@link #annotation} must reference the meta-annotation.</li>
|
||||
* <li>The referenced meta-annotation must be <em>meta-present</em> on the
|
||||
* annotation class that declares {@code @AliasFor}.</li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Example: Aliases within an Annotation</h3>
|
||||
* <h3>Example: Explicit Aliases within an Annotation</h3>
|
||||
* <pre class="code"> public @interface ContextConfiguration {
|
||||
*
|
||||
* @AliasFor("locations")
|
||||
|
@ -93,7 +111,7 @@ import java.lang.annotation.Target;
|
|||
* // ...
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Example: Alias for Attribute in Meta-annotation</h3>
|
||||
* <h3>Example: Explicit Alias for Attribute in Meta-annotation</h3>
|
||||
* <pre class="code"> @ContextConfiguration
|
||||
* public @interface MyTestConfig {
|
||||
*
|
||||
|
@ -101,6 +119,20 @@ import java.lang.annotation.Target;
|
|||
* String[] xmlFiles();
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Example: Implicit Aliases within an Annotation</h3>
|
||||
* <pre class="code"> @ContextConfiguration
|
||||
* public @interface MyTestConfig {
|
||||
*
|
||||
* @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
|
||||
* String[] value() default {};
|
||||
*
|
||||
* @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
|
||||
* String[] groovyScripts() default {};
|
||||
*
|
||||
* @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
|
||||
* String[] xmlFiles() default {};
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Spring Annotations Supporting Attribute Aliases</h3>
|
||||
* <p>As of Spring Framework 4.2, several annotations within core Spring
|
||||
* have been updated to use {@code @AliasFor} to configure their internal
|
||||
|
|
|
@ -31,7 +31,6 @@ import org.springframework.core.BridgeMethodResolver;
|
|||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* General utility methods for finding annotations and meta-annotations on
|
||||
|
@ -957,13 +956,21 @@ public class AnnotatedElementUtils {
|
|||
|
||||
for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {
|
||||
String attributeName = attributeMethod.getName();
|
||||
String aliasedAttributeName = AnnotationUtils.getAliasedAttributeName(attributeMethod,
|
||||
targetAnnotationType);
|
||||
List<String> aliases = AnnotationUtils.getAliasedAttributeNames(attributeMethod, targetAnnotationType);
|
||||
|
||||
// Explicit annotation attribute override declared via @AliasFor
|
||||
if (StringUtils.hasText(aliasedAttributeName) && attributes.containsKey(aliasedAttributeName)) {
|
||||
overrideAttribute(element, annotation, attributes, attributeName, aliasedAttributeName);
|
||||
if (!aliases.isEmpty()) {
|
||||
if (aliases.size() != 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Alias list for annotation attribute [%s] must contain at most one element: %s",
|
||||
attributeMethod, aliases));
|
||||
}
|
||||
String aliasedAttributeName = aliases.get(0);
|
||||
if (attributes.containsKey(aliasedAttributeName)) {
|
||||
overrideAttribute(element, annotation, attributes, attributeName, aliasedAttributeName);
|
||||
}
|
||||
}
|
||||
|
||||
// Implicit annotation attribute override based on convention
|
||||
else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
|
||||
overrideAttribute(element, annotation, attributes, attributeName, attributeName);
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.lang.reflect.AnnotatedElement;
|
|||
import java.lang.reflect.Array;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -422,26 +423,38 @@ public class AnnotationAttributes extends LinkedHashMap<String, Object> {
|
|||
Assert.notNull(expectedType, "expectedType must not be null");
|
||||
|
||||
T attributeValue = getAttribute(attributeName, expectedType);
|
||||
String aliasName = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
|
||||
T aliasValue = getAttribute(aliasName, expectedType);
|
||||
boolean attributeDeclared = !ObjectUtils.isEmpty(attributeValue);
|
||||
boolean aliasDeclared = !ObjectUtils.isEmpty(aliasValue);
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) && attributeDeclared && aliasDeclared) {
|
||||
String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString());
|
||||
String msg = String.format("In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] " +
|
||||
"are present with values of [%s] and [%s], but only one is permitted.",
|
||||
annotationType.getName(), elementName, attributeName, aliasName,
|
||||
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue));
|
||||
throw new AnnotationConfigurationException(msg);
|
||||
List<String> aliasNames = AnnotationUtils.getAttributeAliasMap(annotationType).get(attributeName);
|
||||
if (aliasNames != null) {
|
||||
for (String aliasName : aliasNames) {
|
||||
T aliasValue = getAttribute(aliasName, expectedType);
|
||||
boolean attributeEmpty = ObjectUtils.isEmpty(attributeValue);
|
||||
boolean aliasEmpty = ObjectUtils.isEmpty(aliasValue);
|
||||
|
||||
if (!attributeEmpty && !aliasEmpty && !ObjectUtils.nullSafeEquals(attributeValue, aliasValue)) {
|
||||
String elementName = (annotationSource == null ? "unknown element" : annotationSource.toString());
|
||||
String msg = String.format("In annotation [%s] declared on [%s], attribute [%s] and its alias [%s] " +
|
||||
"are present with values of [%s] and [%s], but only one is permitted.",
|
||||
annotationType.getName(), elementName, attributeName, aliasName,
|
||||
ObjectUtils.nullSafeToString(attributeValue), ObjectUtils.nullSafeToString(aliasValue));
|
||||
throw new AnnotationConfigurationException(msg);
|
||||
}
|
||||
|
||||
// If we expect an array and the current tracked value is null but the
|
||||
// current alias value is non-null, then replace the current null value
|
||||
// with the non-null value (which may be an empty array).
|
||||
if (expectedType.isArray() && attributeValue == null && aliasValue != null) {
|
||||
attributeValue = aliasValue;
|
||||
}
|
||||
// Else: if we're not expecting an array, we can rely on the behavior of
|
||||
// ObjectUtils.isEmpty().
|
||||
else if (attributeEmpty && !aliasEmpty) {
|
||||
attributeValue = aliasValue;
|
||||
}
|
||||
}
|
||||
assertAttributePresence(attributeName, aliasNames, attributeValue);
|
||||
}
|
||||
|
||||
if (!attributeDeclared) {
|
||||
attributeValue = aliasValue;
|
||||
}
|
||||
|
||||
assertAttributePresence(attributeName, aliasName, 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) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Neither attribute '%s' nor its alias '%s' was found in attributes for annotation [%s]",
|
||||
attributeName, aliasName, this.displayName));
|
||||
"Neither attribute '%s' nor one of its aliases %s was found in attributes for annotation [%s]",
|
||||
attributeName, aliases, this.displayName));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,9 @@ import org.springframework.util.StringUtils;
|
|||
*
|
||||
* <p>An annotation is <em>meta-present</em> on an element if the annotation
|
||||
* is declared as a meta-annotation on some other annotation which is
|
||||
* <em>present</em> on the element.
|
||||
* <em>present</em> on the element. Annotation {@code A} is <em>meta-present</em>
|
||||
* on another annotation if {@code A} is either <em>directly present</em> or
|
||||
* <em>meta-present</em> on the other annotation.
|
||||
*
|
||||
* <h3>Meta-annotation Support</h3>
|
||||
* <p>Most {@code find*()} methods and some {@code get*()} methods in this
|
||||
|
@ -123,11 +125,14 @@ public abstract class AnnotationUtils {
|
|||
private static final Map<Class<?>, Boolean> annotatedInterfaceCache =
|
||||
new ConcurrentReferenceHashMap<Class<?>, Boolean>(256);
|
||||
|
||||
private static final Map<AnnotationCacheKey, Boolean> metaPresentCache =
|
||||
new ConcurrentReferenceHashMap<AnnotationCacheKey, Boolean>(256);
|
||||
|
||||
private static final Map<Class<? extends Annotation>, Boolean> synthesizableCache =
|
||||
new ConcurrentReferenceHashMap<Class<? extends Annotation>, Boolean>(256);
|
||||
|
||||
private static final Map<Class<? extends Annotation>, Map<String, String>> attributeAliasesCache =
|
||||
new ConcurrentReferenceHashMap<Class<? extends Annotation>, Map<String, String>>(256);
|
||||
private static final Map<Class<? extends Annotation>, Map<String, List<String>>> attributeAliasesCache =
|
||||
new ConcurrentReferenceHashMap<Class<? extends Annotation>, Map<String, List<String>>>(256);
|
||||
|
||||
private static final Map<Class<? extends Annotation>, List<Method>> attributeMethodsCache =
|
||||
new ConcurrentReferenceHashMap<Class<? extends Annotation>, List<Method>>(256);
|
||||
|
@ -643,8 +648,22 @@ public abstract class AnnotationUtils {
|
|||
* @param annotationType the type of annotation to look for
|
||||
* @return the first matching annotation, or {@code null} if not found
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) {
|
||||
return findAnnotation(clazz, annotationType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual work for {@link #findAnnotation(AnnotatedElement, Class)},
|
||||
* honoring the {@code synthesize} flag.
|
||||
* @param clazz the class to look for annotations on; never {@code null}
|
||||
* @param annotationType the type of annotation to look for
|
||||
* @param synthesize {@code true} if the result should be
|
||||
* {@linkplain #synthesizeAnnotation(Annotation) synthesized}
|
||||
* @return the first matching annotation, or {@code null} if not found
|
||||
* @since 4.2.1
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, boolean synthesize) {
|
||||
AnnotationCacheKey cacheKey = new AnnotationCacheKey(clazz, annotationType);
|
||||
A result = (A) findAnnotationCache.get(cacheKey);
|
||||
if (result == null) {
|
||||
|
@ -653,7 +672,7 @@ public abstract class AnnotationUtils {
|
|||
findAnnotationCache.put(cacheKey, result);
|
||||
}
|
||||
}
|
||||
return synthesizeAnnotation(result, clazz);
|
||||
return (synthesize ? synthesizeAnnotation(result, clazz) : result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -833,6 +852,30 @@ public abstract class AnnotationUtils {
|
|||
return (clazz.isAnnotationPresent(annotationType) && !isAnnotationDeclaredLocally(annotationType, clazz));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an annotation of type {@code metaAnnotationType} is
|
||||
* <em>meta-present</em> on the supplied {@code annotationType}.
|
||||
* @param annotationType the annotation type to search on; never {@code null}
|
||||
* @param metaAnnotationType the type of meta-annotation to search for
|
||||
* @return {@code true} if such an annotation is meta-present
|
||||
* @since 4.2.1
|
||||
*/
|
||||
public static boolean isAnnotationMetaPresent(Class<? extends Annotation> annotationType,
|
||||
Class<? extends Annotation> metaAnnotationType) {
|
||||
|
||||
AnnotationCacheKey cacheKey = new AnnotationCacheKey(annotationType, metaAnnotationType);
|
||||
Boolean metaPresent = metaPresentCache.get(cacheKey);
|
||||
if (metaPresent != null) {
|
||||
return metaPresent.booleanValue();
|
||||
}
|
||||
metaPresent = Boolean.FALSE;
|
||||
if (findAnnotation(annotationType, metaAnnotationType, false) != null) {
|
||||
metaPresent = Boolean.TRUE;
|
||||
}
|
||||
metaPresentCache.put(cacheKey, metaPresent);
|
||||
return metaPresent.booleanValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the supplied {@link Annotation} is defined in the core JDK
|
||||
* {@code java.lang.annotation} package.
|
||||
|
@ -1363,33 +1406,39 @@ public abstract class AnnotationUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get a map of all attribute alias pairs, declared via {@code @AliasFor}
|
||||
* Get a map of all attribute aliases declared via {@code @AliasFor}
|
||||
* in the supplied annotation type.
|
||||
* <p>The map is keyed by attribute name with each value representing
|
||||
* the name of the aliased attribute. For each entry {@code [x, y]} in
|
||||
* the map there will be a corresponding {@code [y, x]} entry in the map.
|
||||
* a list of names of aliased attributes.
|
||||
* <p>For <em>explicit</em> alias pairs such as x and y (i.e., where x
|
||||
* is an {@code @AliasFor("y")} and y is an {@code @AliasFor("x")}, there
|
||||
* will be two entries in the map: {@code x -> (y)} and {@code y -> (x)}.
|
||||
* <p>For <em>implicit</em> aliases (i.e., attributes that are declared
|
||||
* as attribute overrides for the same attribute in the same meta-annotation),
|
||||
* there will be n entries in the map. For example, if x, y, and z are
|
||||
* implicit aliases, the map will contain the following entries:
|
||||
* {@code x -> (y, z)}, {@code y -> (x, z)}, {@code z -> (x, y)}.
|
||||
* <p>An empty return value implies that the annotation does not declare
|
||||
* any attribute aliases.
|
||||
* @param annotationType the annotation type to find attribute aliases in
|
||||
* @return a map containing attribute alias pairs; never {@code null}
|
||||
* @return a map containing attribute aliases; never {@code null}
|
||||
* @since 4.2
|
||||
*/
|
||||
static Map<String, String> getAttributeAliasMap(Class<? extends Annotation> annotationType) {
|
||||
static Map<String, List<String>> getAttributeAliasMap(Class<? extends Annotation> annotationType) {
|
||||
if (annotationType == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<String, String> map = attributeAliasesCache.get(annotationType);
|
||||
Map<String, List<String>> map = attributeAliasesCache.get(annotationType);
|
||||
if (map != null) {
|
||||
return map;
|
||||
}
|
||||
|
||||
map = new HashMap<String, String>();
|
||||
map = new HashMap<String, List<String>>();
|
||||
for (Method attribute : getAttributeMethods(annotationType)) {
|
||||
String attributeName = attribute.getName();
|
||||
String aliasedAttributeName = getAliasedAttributeName(attribute);
|
||||
if (aliasedAttributeName != null) {
|
||||
map.put(attributeName, aliasedAttributeName);
|
||||
List<String> aliasNames = getAliasedAttributeNames(attribute);
|
||||
if (!aliasNames.isEmpty()) {
|
||||
map.put(attribute.getName(), aliasNames);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1420,7 +1469,7 @@ public abstract class AnnotationUtils {
|
|||
|
||||
synthesizable = Boolean.FALSE;
|
||||
for (Method attribute : getAttributeMethods(annotationType)) {
|
||||
if (getAliasedAttributeName(attribute) != null) {
|
||||
if (!getAliasedAttributeNames(attribute).isEmpty()) {
|
||||
synthesizable = Boolean.TRUE;
|
||||
break;
|
||||
}
|
||||
|
@ -1446,184 +1495,85 @@ public abstract class AnnotationUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the name of the aliased attribute configured via
|
||||
* {@link AliasFor @AliasFor} on the supplied annotation {@code attribute}.
|
||||
* <p>This method does not resolve aliases in other annotations. In
|
||||
* other words, if {@code @AliasFor} is present on the supplied
|
||||
* {@code attribute} but {@linkplain AliasFor#annotation references an
|
||||
* annotation} other than {@link Annotation}, this method will return
|
||||
* {@code null} immediately.
|
||||
* @param attribute the attribute to find an alias for
|
||||
* @return the name of the aliased attribute, or {@code null} if not found
|
||||
* Get the names of the aliased attributes configured via
|
||||
* {@link AliasFor @AliasFor} for the supplied annotation {@code attribute}.
|
||||
* <p>This method does not resolve meta-annotation attribute overrides.
|
||||
* @param attribute the attribute to find aliases for; never {@code null}
|
||||
* @return the names of the aliased attributes; never {@code null}, though
|
||||
* potentially <em>empty</em>
|
||||
* @throws IllegalArgumentException if the supplied attribute method is
|
||||
* not from an annotation, or if the supplied target type is {@link Annotation}
|
||||
* {@code null} or not from an annotation
|
||||
* @throws AnnotationConfigurationException if invalid configuration of
|
||||
* {@code @AliasFor} is detected
|
||||
* @since 4.2
|
||||
* @see #getAliasedAttributeName(Method, Class)
|
||||
* @see #getAliasedAttributeNames(Method, Class)
|
||||
*/
|
||||
static String getAliasedAttributeName(Method attribute) {
|
||||
return getAliasedAttributeName(attribute, (Class<? extends Annotation>) null);
|
||||
static List<String> getAliasedAttributeNames(Method attribute) {
|
||||
return getAliasedAttributeNames(attribute, (Class<? extends Annotation>) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the aliased attribute configured via
|
||||
* {@link AliasFor @AliasFor} on the supplied annotation {@code attribute}.
|
||||
* @param attribute the attribute to find an alias for
|
||||
* @param targetAnnotationType the type of annotation in which the
|
||||
* Get the names of the aliased attributes configured via
|
||||
* {@link AliasFor @AliasFor} for the supplied annotation {@code attribute}.
|
||||
* <p>If the supplied {@code metaAnnotationType} is non-null, the
|
||||
* returned list will contain at most one element.
|
||||
* @param attribute the attribute to find aliases for; never {@code null}
|
||||
* @param metaAnnotationType the type of meta-annotation in which an
|
||||
* aliased attribute is allowed to be declared; {@code null} implies
|
||||
* <em>within the same annotation</em>
|
||||
* @return the name of the aliased attribute, or {@code null} if not found
|
||||
* <em>within the same annotation</em> as the supplied attribute
|
||||
* @return the names of the aliased attributes; never {@code null}, though
|
||||
* potentially <em>empty</em>
|
||||
* @throws IllegalArgumentException if the supplied attribute method is
|
||||
* not from an annotation, or if the supplied target type is {@link Annotation}
|
||||
* {@code null} or not from an annotation, or if the supplied meta-annotation
|
||||
* type is {@link Annotation}
|
||||
* @throws AnnotationConfigurationException if invalid configuration of
|
||||
* {@code @AliasFor} is detected
|
||||
* @since 4.2
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static String getAliasedAttributeName(Method attribute, Class<? extends Annotation> targetAnnotationType) {
|
||||
Class<?> declaringClass = attribute.getDeclaringClass();
|
||||
Assert.isTrue(declaringClass.isAnnotation(), "attribute method must be from an annotation");
|
||||
Assert.isTrue(!Annotation.class.equals(targetAnnotationType),
|
||||
"targetAnnotationType must not be java.lang.annotation.Annotation");
|
||||
static List<String> getAliasedAttributeNames(Method attribute, Class<? extends Annotation> metaAnnotationType) {
|
||||
Assert.notNull(attribute, "attribute method must not be null");
|
||||
Assert.isTrue(!Annotation.class.equals(metaAnnotationType),
|
||||
"metaAnnotationType must not be java.lang.annotation.Annotation");
|
||||
|
||||
String attributeName = attribute.getName();
|
||||
AliasFor aliasFor = attribute.getAnnotation(AliasFor.class);
|
||||
AliasDescriptor descriptor = AliasDescriptor.from(attribute);
|
||||
|
||||
// Nothing to check
|
||||
if (aliasFor == null) {
|
||||
return null;
|
||||
// No alias declared via @AliasFor?
|
||||
if (descriptor == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Class<? extends Annotation> sourceAnnotationType = (Class<? extends Annotation>) declaringClass;
|
||||
Class<? extends Annotation> aliasedAnnotationType = aliasFor.annotation();
|
||||
|
||||
boolean searchWithinSameAnnotation = (targetAnnotationType == null);
|
||||
boolean sameTargetDeclared =
|
||||
(sourceAnnotationType.equals(aliasedAnnotationType) || Annotation.class.equals(aliasedAnnotationType));
|
||||
|
||||
// Explicit alias for a different target meta-annotation?
|
||||
if (!searchWithinSameAnnotation && !targetAnnotationType.equals(aliasedAnnotationType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String aliasedAttributeName = getAliasedAttributeName(aliasFor, attribute);
|
||||
|
||||
if (!StringUtils.hasText(aliasedAttributeName)) {
|
||||
String msg = String.format(
|
||||
"@AliasFor declaration on attribute [%s] in annotation [%s] is missing required 'attribute' value.",
|
||||
attributeName, sourceAnnotationType.getName());
|
||||
throw new AnnotationConfigurationException(msg);
|
||||
}
|
||||
|
||||
if (!sameTargetDeclared) {
|
||||
// Target annotation is not meta-present?
|
||||
if (findAnnotation(sourceAnnotationType, aliasedAnnotationType) == null) {
|
||||
String msg = String.format("@AliasFor declaration on attribute [%s] in annotation [%s] declares "
|
||||
+ "an alias for attribute [%s] in meta-annotation [%s] which is not meta-present.",
|
||||
attributeName, sourceAnnotationType.getName(), aliasedAttributeName,
|
||||
aliasedAnnotationType.getName());
|
||||
throw new AnnotationConfigurationException(msg);
|
||||
// Searching for explicit meta-annotation attribute override?
|
||||
if (metaAnnotationType != null) {
|
||||
if (descriptor.isAliasFor(metaAnnotationType)) {
|
||||
return Collections.singletonList(descriptor.aliasedAttributeName);
|
||||
}
|
||||
}
|
||||
else {
|
||||
aliasedAnnotationType = sourceAnnotationType;
|
||||
// Else: explicit attribute override for a different meta-annotation
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Wrong search scope?
|
||||
if (searchWithinSameAnnotation && !sameTargetDeclared) {
|
||||
return null;
|
||||
// Explicit alias pair?
|
||||
if (descriptor.isAliasPair) {
|
||||
return Collections.singletonList(descriptor.aliasedAttributeName);
|
||||
}
|
||||
|
||||
Method aliasedAttribute;
|
||||
try {
|
||||
aliasedAttribute = aliasedAnnotationType.getDeclaredMethod(aliasedAttributeName);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
String msg = String.format(
|
||||
"Attribute [%s] in annotation [%s] is declared as an @AliasFor nonexistent attribute [%s] in annotation [%s].",
|
||||
attributeName, sourceAnnotationType.getName(), aliasedAttributeName, aliasedAnnotationType.getName());
|
||||
throw new AnnotationConfigurationException(msg, ex);
|
||||
}
|
||||
// Else: search for implicit aliases
|
||||
List<String> aliases = new ArrayList<String>();
|
||||
for (Method currentAttribute : getAttributeMethods(descriptor.sourceAnnotationType)) {
|
||||
|
||||
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);
|
||||
// An attribute cannot alias itself
|
||||
if (attribute.equals(currentAttribute)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
// If two attributes override the same attribute in the same meta-annotation,
|
||||
// they are "implicit" aliases for each other.
|
||||
AliasDescriptor otherDescriptor = AliasDescriptor.from(currentAttribute);
|
||||
if (descriptor.equals(otherDescriptor)) {
|
||||
descriptor.validateAgainst(otherDescriptor);
|
||||
aliases.add(otherDescriptor.sourceAttributeName);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1677,6 +1627,7 @@ public abstract class AnnotationUtils {
|
|||
* Determine if the supplied {@code method} is an annotation attribute method.
|
||||
* @param method the method to check
|
||||
* @return {@code true} if the method is an attribute method
|
||||
* @since 4.2
|
||||
*/
|
||||
static boolean isAttributeMethod(Method method) {
|
||||
return (method != null && method.getParameterTypes().length == 0 && method.getReturnType() != void.class);
|
||||
|
@ -1686,6 +1637,7 @@ public abstract class AnnotationUtils {
|
|||
* Determine if the supplied method is an "annotationType" method.
|
||||
* @return {@code true} if the method is an "annotationType" method
|
||||
* @see Annotation#annotationType()
|
||||
* @since 4.2
|
||||
*/
|
||||
static boolean isAnnotationTypeMethod(Method method) {
|
||||
return (method != null && method.getName().equals("annotationType") && method.getParameterTypes().length == 0);
|
||||
|
@ -1723,40 +1675,62 @@ public abstract class AnnotationUtils {
|
|||
|
||||
Class<? extends Annotation> annotationType = attributes.annotationType();
|
||||
|
||||
// Track which attribute values have already been replaced so that we can short
|
||||
// circuit the search algorithms.
|
||||
Set<String> valuesAlreadyReplaced = new HashSet<String>();
|
||||
|
||||
// Validate @AliasFor configuration
|
||||
Map<String, String> aliasMap = getAttributeAliasMap(annotationType);
|
||||
Set<String> validated = new HashSet<String>();
|
||||
Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
|
||||
for (String attributeName : aliasMap.keySet()) {
|
||||
String aliasedAttributeName = aliasMap.get(attributeName);
|
||||
if (valuesAlreadyReplaced.contains(attributeName)) {
|
||||
continue;
|
||||
}
|
||||
Object value = attributes.get(attributeName);
|
||||
boolean valuePresent = (value != null && value != DEFAULT_VALUE_PLACEHOLDER);
|
||||
|
||||
for (String aliasedAttributeName : aliasMap.get(attributeName)) {
|
||||
if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validated.add(attributeName) && validated.add(aliasedAttributeName)) {
|
||||
Object value = attributes.get(attributeName);
|
||||
Object aliasedValue = attributes.get(aliasedAttributeName);
|
||||
boolean aliasPresent = (aliasedValue != null && aliasedValue != DEFAULT_VALUE_PLACEHOLDER);
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(value, aliasedValue) && (value != DEFAULT_VALUE_PLACEHOLDER)
|
||||
&& (aliasedValue != DEFAULT_VALUE_PLACEHOLDER)) {
|
||||
String elementAsString = (element == null ? "unknown element" : element.toString());
|
||||
String msg = String.format(
|
||||
"In AnnotationAttributes for annotation [%s] declared on [%s], attribute [%s] and its alias [%s] are "
|
||||
+ "declared with values of [%s] and [%s], but only one declaration is permitted.",
|
||||
annotationType.getName(), elementAsString, attributeName, aliasedAttributeName,
|
||||
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue));
|
||||
throw new AnnotationConfigurationException(msg);
|
||||
}
|
||||
|
||||
// Replace default values with aliased values...
|
||||
if (value == DEFAULT_VALUE_PLACEHOLDER) {
|
||||
attributes.put(attributeName,
|
||||
adaptValue(element, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
|
||||
}
|
||||
if (aliasedValue == DEFAULT_VALUE_PLACEHOLDER) {
|
||||
attributes.put(aliasedAttributeName,
|
||||
adaptValue(element, value, classValuesAsString, nestedAnnotationsAsMap));
|
||||
// Something to validate or replace with an alias?
|
||||
if (valuePresent || aliasPresent) {
|
||||
if (valuePresent && aliasPresent) {
|
||||
// Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
|
||||
if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
|
||||
String elementAsString = (element == null ? "unknown element" : element.toString());
|
||||
String msg = String.format("In AnnotationAttributes for annotation [%s] declared on [%s], "
|
||||
+ "attribute [%s] and its alias [%s] are declared with values of [%s] and [%s], "
|
||||
+ "but only one declaration is permitted.", annotationType.getName(),
|
||||
elementAsString, attributeName, aliasedAttributeName,
|
||||
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(aliasedValue));
|
||||
throw new AnnotationConfigurationException(msg);
|
||||
}
|
||||
}
|
||||
else if (aliasPresent) {
|
||||
// Replace value with aliasedValue
|
||||
attributes.put(attributeName,
|
||||
adaptValue(element, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
|
||||
valuesAlreadyReplaced.add(attributeName);
|
||||
}
|
||||
else {
|
||||
// Replace aliasedValue with value
|
||||
attributes.put(aliasedAttributeName,
|
||||
adaptValue(element, value, classValuesAsString, nestedAnnotationsAsMap));
|
||||
valuesAlreadyReplaced.add(aliasedAttributeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any remaining placeholders with actual default values
|
||||
for (String attributeName : attributes.keySet()) {
|
||||
if (valuesAlreadyReplaced.contains(attributeName)) {
|
||||
continue;
|
||||
}
|
||||
Object value = attributes.get(attributeName);
|
||||
if (value == DEFAULT_VALUE_PLACEHOLDER) {
|
||||
attributes.put(attributeName,
|
||||
|
@ -1933,4 +1907,248 @@ public abstract class AnnotationUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code AliasDescriptor} encapsulates the declaration of {@code @AliasFor}
|
||||
* on a given annotation attribute and includes support for validating
|
||||
* the configuration of aliases (both explicit and implicit).
|
||||
* @since 4.2.1
|
||||
*/
|
||||
private static class AliasDescriptor {
|
||||
|
||||
private final Method sourceAttribute;
|
||||
|
||||
private final Class<? 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
|
|||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
@ -87,25 +88,30 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib
|
|||
Map<String, Object> originalAttributes, Class<? extends Annotation> annotationType) {
|
||||
|
||||
Map<String, Object> attributes = new HashMap<String, Object>(originalAttributes);
|
||||
Map<String, String> attributeAliasMap = getAttributeAliasMap(annotationType);
|
||||
Map<String, List<String>> attributeAliasMap = getAttributeAliasMap(annotationType);
|
||||
|
||||
for (Method attributeMethod : getAttributeMethods(annotationType)) {
|
||||
String attributeName = attributeMethod.getName();
|
||||
Object attributeValue = attributes.get(attributeName);
|
||||
|
||||
// if attribute not present, check alias
|
||||
// if attribute not present, check aliases
|
||||
if (attributeValue == null) {
|
||||
String aliasName = attributeAliasMap.get(attributeName);
|
||||
if (aliasName != null) {
|
||||
Object aliasValue = attributes.get(aliasName);
|
||||
if (aliasValue != null) {
|
||||
attributeValue = aliasValue;
|
||||
attributes.put(attributeName, attributeValue);
|
||||
List<String> aliasNames = attributeAliasMap.get(attributeName);
|
||||
if (aliasNames != null) {
|
||||
for (String aliasName : aliasNames) {
|
||||
if (aliasName != null) {
|
||||
Object aliasValue = attributes.get(aliasName);
|
||||
if (aliasValue != null) {
|
||||
attributeValue = aliasValue;
|
||||
attributes.put(attributeName, attributeValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if alias not present, check default
|
||||
// if aliases not present, check default
|
||||
if (attributeValue == null) {
|
||||
Object defaultValue = getDefaultValue(annotationType, attributeName);
|
||||
if (defaultValue != null) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -323,16 +323,62 @@ public class AnnotatedElementUtilsTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void getMergeAndSynthesizeAnnotationWithAliasedValueComposedAnnotation() {
|
||||
Class<?> element = AliasedValueComposedContextConfigClass.class;
|
||||
public void getMergedAnnotationAttributesWithImplicitAliasesInMetaAnnotationOnComposedAnnotation() {
|
||||
Class<?> element = ComposedImplicitAliasesContextConfigClass.class;
|
||||
String name = ImplicitAliasesContextConfig.class.getName();
|
||||
AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name);
|
||||
String[] expected = new String[] { "A.xml", "B.xml" };
|
||||
|
||||
assertNotNull("Should find @ImplicitAliasesContextConfig on " + element.getSimpleName(), attributes);
|
||||
assertArrayEquals("groovyScripts", expected, attributes.getStringArray("groovyScripts"));
|
||||
assertArrayEquals("xmlFiles", expected, attributes.getStringArray("xmlFiles"));
|
||||
assertArrayEquals("locations", expected, attributes.getStringArray("locations"));
|
||||
assertArrayEquals("value", expected, attributes.getStringArray("value"));
|
||||
|
||||
// Verify contracts between utility methods:
|
||||
assertTrue(isAnnotated(element, name));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMergedAnnotationWithAliasedValueComposedAnnotation() {
|
||||
assertGetMergedAnnotation(AliasedValueComposedContextConfigClass.class, "test.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMergedAnnotationWithImplicitAliasesForSameAttributeInComposedAnnotation() {
|
||||
assertGetMergedAnnotation(ImplicitAliasesContextConfigClass1.class, "foo.xml");
|
||||
assertGetMergedAnnotation(ImplicitAliasesContextConfigClass2.class, "bar.xml");
|
||||
assertGetMergedAnnotation(ImplicitAliasesContextConfigClass3.class, "baz.xml");
|
||||
}
|
||||
|
||||
private void assertGetMergedAnnotation(Class<?> element, String expected) {
|
||||
String name = ContextConfig.class.getName();
|
||||
ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class);
|
||||
|
||||
assertNotNull("Should find @ContextConfig on " + element.getSimpleName(), contextConfig);
|
||||
assertArrayEquals("locations", new String[] { "test.xml" }, contextConfig.locations());
|
||||
assertArrayEquals("value", new String[] { "test.xml" }, contextConfig.value());
|
||||
assertArrayEquals("locations", new String[] { expected }, contextConfig.locations());
|
||||
assertArrayEquals("value", new String[] { expected }, contextConfig.value());
|
||||
assertArrayEquals("classes", new Class<?>[0], contextConfig.classes());
|
||||
|
||||
// Verify contracts between utility methods:
|
||||
assertTrue(isAnnotated(element, ContextConfig.class.getName()));
|
||||
assertTrue(isAnnotated(element, name));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMergedAnnotationWithImplicitAliasesInMetaAnnotationOnComposedAnnotation() {
|
||||
Class<?> element = ComposedImplicitAliasesContextConfigClass.class;
|
||||
String name = ImplicitAliasesContextConfig.class.getName();
|
||||
ImplicitAliasesContextConfig config = getMergedAnnotation(element, ImplicitAliasesContextConfig.class);
|
||||
String[] expected = new String[] { "A.xml", "B.xml" };
|
||||
|
||||
assertNotNull("Should find @ImplicitAliasesContextConfig on " + element.getSimpleName(), config);
|
||||
assertArrayEquals("groovyScripts", expected, config.groovyScripts());
|
||||
assertArrayEquals("xmlFiles", expected, config.xmlFiles());
|
||||
assertArrayEquals("locations", expected, config.locations());
|
||||
assertArrayEquals("value", expected, config.value());
|
||||
|
||||
// Verify contracts between utility methods:
|
||||
assertTrue(isAnnotated(element, name));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -517,11 +563,11 @@ public class AnnotatedElementUtilsTests {
|
|||
|
||||
@Test
|
||||
public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
|
||||
String[] expected = new String[] { "com.example.app.test" };
|
||||
Class<?> element = TestComponentScanClass.class;
|
||||
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
|
||||
assertNotNull("Should find @ComponentScan on " + element, attributes);
|
||||
assertArrayEquals("basePackages for " + element, new String[] { "com.example.app.test" },
|
||||
attributes.getStringArray("basePackages"));
|
||||
assertArrayEquals("basePackages for " + element, expected, attributes.getStringArray("basePackages"));
|
||||
|
||||
Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class);
|
||||
assertNotNull(excludeFilters);
|
||||
|
@ -530,6 +576,22 @@ public class AnnotatedElementUtilsTests {
|
|||
assertEquals(asList("*Test", "*Tests"), patterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* This test ensures that {@link AnnotationUtils#postProcessAnnotationAttributes}
|
||||
* uses {@code ObjectUtils.nullSafeEquals()} to check for equality between annotation
|
||||
* attributes since attributes may be arrays.
|
||||
*/
|
||||
@Test
|
||||
public void findMergedAnnotationAttributesOnClassWithBothAttributesOfAnAliasPairDeclared() {
|
||||
String[] expected = new String[] { "com.example.app.test" };
|
||||
Class<?> element = ComponentScanWithBasePackagesAndValueAliasClass.class;
|
||||
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
|
||||
|
||||
assertNotNull("Should find @ComponentScan on " + element, attributes);
|
||||
assertArrayEquals("value: ", expected, attributes.getStringArray("value"));
|
||||
assertArrayEquals("basePackages: ", expected, attributes.getStringArray("basePackages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() {
|
||||
final String[] EMPTY = new String[] {};
|
||||
|
@ -716,6 +778,28 @@ public class AnnotatedElementUtilsTests {
|
|||
String[] locations();
|
||||
}
|
||||
|
||||
@ContextConfig
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ImplicitAliasesContextConfig {
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
|
||||
String[] groovyScripts() default {};
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
|
||||
String[] xmlFiles() default {};
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
|
||||
String[] locations() default {};
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "locations")
|
||||
String[] value() default {};
|
||||
}
|
||||
|
||||
@ImplicitAliasesContextConfig(xmlFiles = { "A.xml", "B.xml" })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ComposedImplicitAliasesContextConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid because the configuration declares a value for 'value' and
|
||||
* requires a value for the aliased 'locations'. So we likely end up with
|
||||
|
@ -762,6 +846,10 @@ public class AnnotatedElementUtilsTests {
|
|||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ComponentScan {
|
||||
|
||||
@AliasFor("basePackages")
|
||||
String[] value() default {};
|
||||
|
||||
@AliasFor("value")
|
||||
String[] basePackages() default {};
|
||||
|
||||
Filter[] excludeFilters() default {};
|
||||
|
@ -928,6 +1016,22 @@ public class AnnotatedElementUtilsTests {
|
|||
static class AliasedValueComposedContextConfigClass {
|
||||
}
|
||||
|
||||
@ImplicitAliasesContextConfig("foo.xml")
|
||||
static class ImplicitAliasesContextConfigClass1 {
|
||||
}
|
||||
|
||||
@ImplicitAliasesContextConfig(locations = "bar.xml")
|
||||
static class ImplicitAliasesContextConfigClass2 {
|
||||
}
|
||||
|
||||
@ImplicitAliasesContextConfig(xmlFiles = "baz.xml")
|
||||
static class ImplicitAliasesContextConfigClass3 {
|
||||
}
|
||||
|
||||
@ComposedImplicitAliasesContextConfig
|
||||
static class ComposedImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
@InvalidAliasedComposedContextConfig(xmlConfigFiles = "test.xml")
|
||||
static class InvalidAliasedComposedContextConfigClass {
|
||||
}
|
||||
|
@ -936,6 +1040,10 @@ public class AnnotatedElementUtilsTests {
|
|||
static class AliasedComposedContextConfigAndTestPropSourceClass {
|
||||
}
|
||||
|
||||
@ComponentScan(value = "com.example.app.test", basePackages = "com.example.app.test")
|
||||
static class ComponentScanWithBasePackagesAndValueAliasClass {
|
||||
}
|
||||
|
||||
@TestComponentScan(packages = "com.example.app.test")
|
||||
static class TestComponentScanClass {
|
||||
}
|
||||
|
|
|
@ -18,11 +18,16 @@ package org.springframework.core.annotation;
|
|||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtilsTests.ContextConfig;
|
||||
import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
@ -36,7 +41,7 @@ import static org.junit.Assert.*;
|
|||
*/
|
||||
public class AnnotationAttributesTests {
|
||||
|
||||
private final AnnotationAttributes attributes = new AnnotationAttributes();
|
||||
private AnnotationAttributes attributes = new AnnotationAttributes();
|
||||
|
||||
@Rule
|
||||
public final ExpectedException exception = ExpectedException.none();
|
||||
|
@ -156,21 +161,56 @@ public class AnnotationAttributesTests {
|
|||
|
||||
@Test
|
||||
public void getAliasedString() {
|
||||
attributes.clear();
|
||||
attributes.put("name", "metaverse");
|
||||
assertEquals("metaverse", getAliasedString("name"));
|
||||
assertEquals("metaverse", getAliasedString("value"));
|
||||
final String value = "metaverse";
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("value", "metaverse");
|
||||
assertEquals("metaverse", getAliasedString("name"));
|
||||
assertEquals("metaverse", getAliasedString("value"));
|
||||
attributes.put("name", value);
|
||||
assertEquals(value, getAliasedString("name"));
|
||||
assertEquals(value, getAliasedString("value"));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("name", "metaverse");
|
||||
attributes.put("value", "metaverse");
|
||||
assertEquals("metaverse", getAliasedString("name"));
|
||||
assertEquals("metaverse", getAliasedString("value"));
|
||||
attributes.put("value", value);
|
||||
assertEquals(value, getAliasedString("name"));
|
||||
assertEquals(value, 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
|
||||
|
@ -185,7 +225,7 @@ public class AnnotationAttributesTests {
|
|||
@Test
|
||||
public void getAliasedStringWithMissingAliasedAttributes() {
|
||||
exception.expect(IllegalArgumentException.class);
|
||||
exception.expectMessage(equalTo("Neither attribute 'name' nor its alias 'value' was found in attributes for annotation [unknown]"));
|
||||
exception.expectMessage(equalTo("Neither attribute 'name' nor one of its aliases [value] was found in attributes for annotation [unknown]"));
|
||||
getAliasedString("name");
|
||||
}
|
||||
|
||||
|
@ -211,71 +251,135 @@ public class AnnotationAttributesTests {
|
|||
return attrs.getAliasedString(attributeName, Scope.class, null);
|
||||
}
|
||||
|
||||
private String getAliasedStringWithImplicitAliases(String attributeName) {
|
||||
return this.attributes.getAliasedString(attributeName, ImplicitAliasesContextConfig.class, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedStringArray() {
|
||||
final String[] INPUT = new String[] { "test.xml" };
|
||||
final String[] EMPTY = new String[0];
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("locations", INPUT);
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
|
||||
attributes.put("location", INPUT);
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("location"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("value"));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("value", INPUT);
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("location"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("value"));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("locations", INPUT);
|
||||
attributes.put("location", INPUT);
|
||||
attributes.put("value", INPUT);
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("location"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("value"));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("locations", INPUT);
|
||||
attributes.put("location", INPUT);
|
||||
attributes.put("value", EMPTY);
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("location"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("value"));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("locations", EMPTY);
|
||||
attributes.put("location", EMPTY);
|
||||
attributes.put("value", INPUT);
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("locations"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("location"));
|
||||
assertArrayEquals(INPUT, getAliasedStringArray("value"));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("locations", EMPTY);
|
||||
attributes.put("location", EMPTY);
|
||||
attributes.put("value", EMPTY);
|
||||
assertArrayEquals(EMPTY, getAliasedStringArray("locations"));
|
||||
assertArrayEquals(EMPTY, getAliasedStringArray("location"));
|
||||
assertArrayEquals(EMPTY, getAliasedStringArray("value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedStringArrayWithImplicitAliases() {
|
||||
final String[] INPUT = new String[] { "test.xml" };
|
||||
final String[] EMPTY = new String[0];
|
||||
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
|
||||
|
||||
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
|
||||
|
||||
attributes.put("location1", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("value", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", INPUT);
|
||||
attributes.put("value", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", INPUT);
|
||||
attributes.put("value", EMPTY);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", EMPTY);
|
||||
attributes.put("value", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedStringArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", EMPTY);
|
||||
attributes.put("value", EMPTY);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(EMPTY, getAliasedStringArrayWithImplicitAliases(alias)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedStringArrayWithImplicitAliasesWithMissingAliasedAttributes() {
|
||||
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
|
||||
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
|
||||
|
||||
exception.expect(IllegalArgumentException.class);
|
||||
exception.expectMessage(startsWith("Neither attribute 'value' nor one of its aliases ["));
|
||||
aliases.stream().forEach(alias -> exception.expectMessage(containsString(alias)));
|
||||
exception.expectMessage(endsWith("] was found in attributes for annotation [" + ImplicitAliasesContextConfig.class.getName() + "]"));
|
||||
getAliasedStringArrayWithImplicitAliases("value");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedStringArrayWithMissingAliasedAttributes() {
|
||||
exception.expect(IllegalArgumentException.class);
|
||||
exception.expectMessage(equalTo("Neither attribute 'locations' nor its alias 'value' was found in attributes for annotation [unknown]"));
|
||||
getAliasedStringArray("locations");
|
||||
exception.expectMessage(equalTo("Neither attribute 'location' nor one of its aliases [value] was found in attributes for annotation [unknown]"));
|
||||
getAliasedStringArray("location");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedStringArrayWithDifferentAliasedValues() {
|
||||
attributes.put("locations", new String[] { "1.xml" });
|
||||
attributes.put("location", new String[] { "1.xml" });
|
||||
attributes.put("value", new String[] { "2.xml" });
|
||||
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("In annotation [" + ContextConfig.class.getName() + "]"));
|
||||
exception.expectMessage(containsString("attribute [locations] and its alias [value]"));
|
||||
exception.expectMessage(containsString("attribute [location] and its alias [value]"));
|
||||
exception.expectMessage(containsString("[{1.xml}] and [{2.xml}]"));
|
||||
exception.expectMessage(containsString("but only one is permitted"));
|
||||
|
||||
getAliasedStringArray("locations");
|
||||
getAliasedStringArray("location");
|
||||
}
|
||||
|
||||
private String[] getAliasedStringArray(String attributeName) {
|
||||
// Note: even though the attributes we test against here are of type
|
||||
// String instead of String[], it doesn't matter... since
|
||||
// AnnotationAttributes does not validate the actual return type of
|
||||
// attributes in the annotation.
|
||||
return attributes.getAliasedStringArray(attributeName, ContextConfig.class, null);
|
||||
}
|
||||
|
||||
private String[] getAliasedStringArrayWithImplicitAliases(String attributeName) {
|
||||
// Note: even though the attributes we test against here are of type
|
||||
// String instead of String[], it doesn't matter... since
|
||||
// AnnotationAttributes does not validate the actual return type of
|
||||
// attributes in the annotation.
|
||||
return this.attributes.getAliasedStringArray(attributeName, ImplicitAliasesContextConfig.class, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedClassArray() {
|
||||
final Class<?>[] INPUT = new Class<?>[] { String.class };
|
||||
|
@ -316,10 +420,46 @@ public class AnnotationAttributesTests {
|
|||
assertArrayEquals(EMPTY, getAliasedClassArray("value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedClassArrayWithImplicitAliases() {
|
||||
final Class<?>[] INPUT = new Class<?>[] { String.class };
|
||||
final Class<?>[] EMPTY = new Class<?>[0];
|
||||
final List<String> aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript");
|
||||
|
||||
attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class);
|
||||
|
||||
attributes.put("location1", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("value", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", INPUT);
|
||||
attributes.put("value", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", INPUT);
|
||||
attributes.put("value", EMPTY);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", EMPTY);
|
||||
attributes.put("value", INPUT);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(INPUT, getAliasedClassArrayWithImplicitAliases(alias)));
|
||||
|
||||
attributes.clear();
|
||||
attributes.put("location1", EMPTY);
|
||||
attributes.put("value", EMPTY);
|
||||
aliases.stream().forEach(alias -> assertArrayEquals(EMPTY, getAliasedClassArrayWithImplicitAliases(alias)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedClassArrayWithMissingAliasedAttributes() {
|
||||
exception.expect(IllegalArgumentException.class);
|
||||
exception.expectMessage(equalTo("Neither attribute 'classes' nor its alias 'value' was found in attributes for annotation [unknown]"));
|
||||
exception.expectMessage(equalTo("Neither attribute 'classes' nor one of its aliases [value] was found in attributes for annotation [unknown]"));
|
||||
getAliasedClassArray("classes");
|
||||
}
|
||||
|
||||
|
@ -341,6 +481,14 @@ public class AnnotationAttributesTests {
|
|||
return attributes.getAliasedClassArray(attributeName, Filter.class, null);
|
||||
}
|
||||
|
||||
private Class<?>[] getAliasedClassArrayWithImplicitAliases(String attributeName) {
|
||||
// Note: even though the attributes we test against here are of type
|
||||
// String instead of Class<?>[], it doesn't matter... since
|
||||
// AnnotationAttributes does not validate the actual return type of
|
||||
// attributes in the annotation.
|
||||
return this.attributes.getAliasedClassArray(attributeName, ImplicitAliasesContextConfig.class, null);
|
||||
}
|
||||
|
||||
|
||||
enum Color {
|
||||
RED, WHITE, BLUE
|
||||
|
@ -362,19 +510,6 @@ public class AnnotationAttributesTests {
|
|||
static class FilteredClass {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock of {@code org.springframework.test.context.ContextConfiguration}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ContextConfig {
|
||||
|
||||
@AliasFor(attribute = "locations")
|
||||
String value() default "";
|
||||
|
||||
@AliasFor(attribute = "value")
|
||||
String locations() default "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock of {@code org.springframework.context.annotation.Scope}.
|
||||
*/
|
||||
|
|
|
@ -22,14 +22,14 @@ import java.lang.annotation.Repeatable;
|
|||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
@ -38,6 +38,7 @@ import org.springframework.core.Ordered;
|
|||
import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
@ -56,10 +57,31 @@ import static org.springframework.core.annotation.AnnotationUtils.*;
|
|||
*/
|
||||
public class AnnotationUtilsTests {
|
||||
|
||||
static void clearCaches() {
|
||||
clearCache("findAnnotationCache", "annotatedInterfaceCache", "metaPresentCache", "synthesizableCache",
|
||||
"attributeAliasesCache", "attributeMethodsCache");
|
||||
}
|
||||
|
||||
static void clearCache(String... cacheNames) {
|
||||
stream(cacheNames).forEach(cacheName -> getCache(cacheName).clear());
|
||||
}
|
||||
|
||||
static Map<?, ?> getCache(String cacheName) {
|
||||
Field field = ReflectionUtils.findField(AnnotationUtils.class, cacheName);
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
return (Map<?, ?>) ReflectionUtils.getField(field, null);
|
||||
}
|
||||
|
||||
|
||||
@Rule
|
||||
public final ExpectedException exception = ExpectedException.none();
|
||||
|
||||
|
||||
@Before
|
||||
public void clearCachesBeforeTests() {
|
||||
clearCaches();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findMethodAnnotationOnLeaf() throws Exception {
|
||||
Method m = Leaf.class.getMethod("annotatedOnLeaf");
|
||||
|
@ -308,7 +330,7 @@ public class AnnotationUtilsTests {
|
|||
@Test
|
||||
public void findAnnotationDeclaringClassForTypesWithSingleCandidateType() {
|
||||
// no class-level annotation
|
||||
List<Class<? extends Annotation>> transactionalCandidateList = Arrays.<Class<? extends Annotation>> asList(Transactional.class);
|
||||
List<Class<? extends Annotation>> transactionalCandidateList = asList(Transactional.class);
|
||||
assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedInterface.class));
|
||||
assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedClass.class));
|
||||
|
||||
|
@ -323,7 +345,7 @@ public class AnnotationUtilsTests {
|
|||
|
||||
// non-inherited class-level annotation; note: @Order is not inherited,
|
||||
// but findAnnotationDeclaringClassForTypes() should still find it on classes.
|
||||
List<Class<? extends Annotation>> orderCandidateList = Arrays.<Class<? extends Annotation>> asList(Order.class);
|
||||
List<Class<? extends Annotation>> orderCandidateList = asList(Order.class);
|
||||
assertEquals(NonInheritedAnnotationInterface.class,
|
||||
findAnnotationDeclaringClassForTypes(orderCandidateList, NonInheritedAnnotationInterface.class));
|
||||
assertNull(findAnnotationDeclaringClassForTypes(orderCandidateList, SubNonInheritedAnnotationInterface.class));
|
||||
|
@ -335,7 +357,7 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@Test
|
||||
public void findAnnotationDeclaringClassForTypesWithMultipleCandidateTypes() {
|
||||
List<Class<? extends Annotation>> candidates = Arrays.<Class<? extends Annotation>> asList(Transactional.class, Order.class);
|
||||
List<Class<? extends Annotation>> candidates = asList(Transactional.class, Order.class);
|
||||
|
||||
// no class-level annotation
|
||||
assertNull(findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedInterface.class));
|
||||
|
@ -461,7 +483,7 @@ public class AnnotationUtilsTests {
|
|||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("attribute 'value' and its alias 'path'"));
|
||||
exception.expectMessage(containsString("values of [/enigma] and [/test]"));
|
||||
exception.expectMessage(containsString("but only one is permitted"));
|
||||
exception.expectMessage(endsWith("but only one is permitted."));
|
||||
getAnnotationAttributes(webMapping);
|
||||
}
|
||||
|
||||
|
@ -524,21 +546,21 @@ public class AnnotationUtilsTests {
|
|||
Set<MyRepeatable> annotations = getRepeatableAnnotations(method, MyRepeatable.class, MyRepeatableContainer.class);
|
||||
assertNotNull(annotations);
|
||||
List<String> values = annotations.stream().map(MyRepeatable::value).collect(toList());
|
||||
assertThat(values, is(Arrays.asList("A", "B", "C", "meta1")));
|
||||
assertThat(values, is(asList("A", "B", "C", "meta1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRepeatableAnnotationsDeclaredOnClassWithMissingAttributeAliasDeclaration() throws Exception {
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("Attribute [value] in"));
|
||||
exception.expectMessage(startsWith("Attribute [value] in"));
|
||||
exception.expectMessage(containsString(BrokenContextConfig.class.getName()));
|
||||
exception.expectMessage(containsString("must be declared as an @AliasFor [location]"));
|
||||
exception.expectMessage(endsWith("must be declared as an @AliasFor [location]."));
|
||||
getRepeatableAnnotations(BrokenConfigHierarchyTestCase.class, BrokenContextConfig.class, BrokenHierarchy.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getRepeatableAnnotationsDeclaredOnClassWithAttributeAliases() throws Exception {
|
||||
final List<String> expectedLocations = Arrays.asList("A", "B");
|
||||
final List<String> expectedLocations = asList("A", "B");
|
||||
|
||||
Set<ContextConfig> annotations = getRepeatableAnnotations(ConfigHierarchyTestCase.class, ContextConfig.class, null);
|
||||
assertNotNull(annotations);
|
||||
|
@ -556,8 +578,8 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@Test
|
||||
public void getRepeatableAnnotationsDeclaredOnClass() {
|
||||
final List<String> expectedValuesJava = Arrays.asList("A", "B", "C");
|
||||
final List<String> expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1");
|
||||
final List<String> expectedValuesJava = asList("A", "B", "C");
|
||||
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
|
||||
|
||||
// Java 8
|
||||
MyRepeatable[] array = MyRepeatableClass.class.getAnnotationsByType(MyRepeatable.class);
|
||||
|
@ -581,8 +603,8 @@ public class AnnotationUtilsTests {
|
|||
@Test
|
||||
public void getRepeatableAnnotationsDeclaredOnSuperclass() {
|
||||
final Class<?> clazz = SubMyRepeatableClass.class;
|
||||
final List<String> expectedValuesJava = Arrays.asList("A", "B", "C");
|
||||
final List<String> expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1");
|
||||
final List<String> expectedValuesJava = asList("A", "B", "C");
|
||||
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
|
||||
|
||||
// Java 8
|
||||
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
|
||||
|
@ -606,8 +628,8 @@ public class AnnotationUtilsTests {
|
|||
@Test
|
||||
public void getRepeatableAnnotationsDeclaredOnClassAndSuperclass() {
|
||||
final Class<?> clazz = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class;
|
||||
final List<String> expectedValuesJava = Arrays.asList("X", "Y", "Z");
|
||||
final List<String> expectedValuesSpring = Arrays.asList("X", "Y", "Z", "meta2");
|
||||
final List<String> expectedValuesJava = asList("X", "Y", "Z");
|
||||
final List<String> expectedValuesSpring = asList("X", "Y", "Z", "meta2");
|
||||
|
||||
// Java 8
|
||||
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
|
||||
|
@ -631,8 +653,8 @@ public class AnnotationUtilsTests {
|
|||
@Test
|
||||
public void getRepeatableAnnotationsDeclaredOnMultipleSuperclasses() {
|
||||
final Class<?> clazz = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class;
|
||||
final List<String> expectedValuesJava = Arrays.asList("X", "Y", "Z");
|
||||
final List<String> expectedValuesSpring = Arrays.asList("X", "Y", "Z", "meta2");
|
||||
final List<String> expectedValuesJava = asList("X", "Y", "Z");
|
||||
final List<String> expectedValuesSpring = asList("X", "Y", "Z", "meta2");
|
||||
|
||||
// Java 8
|
||||
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
|
||||
|
@ -655,8 +677,8 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@Test
|
||||
public void getDeclaredRepeatableAnnotationsDeclaredOnClass() {
|
||||
final List<String> expectedValuesJava = Arrays.asList("A", "B", "C");
|
||||
final List<String> expectedValuesSpring = Arrays.asList("A", "B", "C", "meta1");
|
||||
final List<String> expectedValuesJava = asList("A", "B", "C");
|
||||
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
|
||||
|
||||
// Java 8
|
||||
MyRepeatable[] array = MyRepeatableClass.class.getDeclaredAnnotationsByType(MyRepeatable.class);
|
||||
|
@ -698,16 +720,45 @@ public class AnnotationUtilsTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedAttributeNameFromWrongTargetAnnotation() throws Exception {
|
||||
public void getAliasedAttributeNamesFromWrongTargetAnnotation() throws Exception {
|
||||
Method attribute = AliasedComposedContextConfig.class.getDeclaredMethod("xmlConfigFile");
|
||||
assertNull("xmlConfigFile is not an alias for @Component.",
|
||||
getAliasedAttributeName(attribute, Component.class));
|
||||
assertThat("xmlConfigFile is not an alias for @Component.",
|
||||
getAliasedAttributeNames(attribute, Component.class), is(empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void 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");
|
||||
assertEquals("location", getAliasedAttributeName(attribute, ContextConfig.class));
|
||||
assertEquals(asList("location"), getAliasedAttributeNames(attribute, ContextConfig.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAliasedAttributeNamesFromComposedAnnotationWithImplicitAliases() throws Exception {
|
||||
Method xmlFile = ImplicitAliasesContextConfig.class.getDeclaredMethod("xmlFile");
|
||||
Method groovyScript = ImplicitAliasesContextConfig.class.getDeclaredMethod("groovyScript");
|
||||
Method value = ImplicitAliasesContextConfig.class.getDeclaredMethod("value");
|
||||
Method location1 = ImplicitAliasesContextConfig.class.getDeclaredMethod("location1");
|
||||
Method location2 = ImplicitAliasesContextConfig.class.getDeclaredMethod("location2");
|
||||
Method location3 = ImplicitAliasesContextConfig.class.getDeclaredMethod("location3");
|
||||
|
||||
// Meta-annotation attribute overrides
|
||||
assertEquals(asList("location"), getAliasedAttributeNames(xmlFile, ContextConfig.class));
|
||||
assertEquals(asList("location"), getAliasedAttributeNames(groovyScript, ContextConfig.class));
|
||||
assertEquals(asList("location"), getAliasedAttributeNames(value, ContextConfig.class));
|
||||
|
||||
// Implicit Aliases
|
||||
assertThat(getAliasedAttributeNames(xmlFile), containsInAnyOrder("value", "groovyScript", "location1", "location2", "location3"));
|
||||
assertThat(getAliasedAttributeNames(groovyScript), containsInAnyOrder("value", "xmlFile", "location1", "location2", "location3"));
|
||||
assertThat(getAliasedAttributeNames(value), containsInAnyOrder("xmlFile", "groovyScript", "location1", "location2", "location3"));
|
||||
assertThat(getAliasedAttributeNames(location1), containsInAnyOrder("xmlFile", "groovyScript", "value", "location2", "location3"));
|
||||
assertThat(getAliasedAttributeNames(location2), containsInAnyOrder("xmlFile", "groovyScript", "value", "location1", "location3"));
|
||||
assertThat(getAliasedAttributeNames(location3), containsInAnyOrder("xmlFile", "groovyScript", "value", "location1", "location2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -746,9 +797,9 @@ public class AnnotationUtilsTests {
|
|||
public void synthesizeAnnotationWhereAliasForIsMissingAttributeDeclaration() throws Exception {
|
||||
AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation(AliasForWithMissingAttributeDeclaration.class);
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("@AliasFor declaration on attribute [foo] in annotation"));
|
||||
exception.expectMessage(startsWith("@AliasFor declaration on attribute [foo] in annotation"));
|
||||
exception.expectMessage(containsString(AliasForWithMissingAttributeDeclaration.class.getName()));
|
||||
exception.expectMessage(containsString("is missing required 'attribute' value"));
|
||||
exception.expectMessage(endsWith("is missing required 'attribute' value."));
|
||||
synthesizeAnnotation(annotation);
|
||||
}
|
||||
|
||||
|
@ -756,10 +807,10 @@ public class AnnotationUtilsTests {
|
|||
public void synthesizeAnnotationWhereAliasForHasDuplicateAttributeDeclaration() throws Exception {
|
||||
AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation(AliasForWithDuplicateAttributeDeclaration.class);
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("In @AliasFor declared on attribute [foo] in annotation"));
|
||||
exception.expectMessage(startsWith("In @AliasFor declared on attribute [foo] in annotation"));
|
||||
exception.expectMessage(containsString(AliasForWithDuplicateAttributeDeclaration.class.getName()));
|
||||
exception.expectMessage(containsString("attribute 'attribute' and its alias 'value' are present with values of [baz] and [bar]"));
|
||||
exception.expectMessage(containsString("but only one is permitted"));
|
||||
exception.expectMessage(endsWith("but only one is permitted."));
|
||||
synthesizeAnnotation(annotation);
|
||||
}
|
||||
|
||||
|
@ -767,7 +818,7 @@ public class AnnotationUtilsTests {
|
|||
public void synthesizeAnnotationWithAttributeAliasForNonexistentAttribute() throws Exception {
|
||||
AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation(AliasForNonexistentAttribute.class);
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("Attribute [foo] in"));
|
||||
exception.expectMessage(startsWith("Attribute [foo] in"));
|
||||
exception.expectMessage(containsString(AliasForNonexistentAttribute.class.getName()));
|
||||
exception.expectMessage(containsString("is declared as an @AliasFor nonexistent attribute [bar]"));
|
||||
synthesizeAnnotation(annotation);
|
||||
|
@ -778,9 +829,9 @@ public class AnnotationUtilsTests {
|
|||
AliasForWithoutMirroredAliasFor annotation =
|
||||
AliasForWithoutMirroredAliasForClass.class.getAnnotation(AliasForWithoutMirroredAliasFor.class);
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("Attribute [bar] in"));
|
||||
exception.expectMessage(startsWith("Attribute [bar] in"));
|
||||
exception.expectMessage(containsString(AliasForWithoutMirroredAliasFor.class.getName()));
|
||||
exception.expectMessage(containsString("must be declared as an @AliasFor [foo]"));
|
||||
exception.expectMessage(endsWith("must be declared as an @AliasFor [foo]."));
|
||||
synthesizeAnnotation(annotation);
|
||||
}
|
||||
|
||||
|
@ -789,12 +840,8 @@ public class AnnotationUtilsTests {
|
|||
AliasForWithMirroredAliasForWrongAttribute annotation =
|
||||
AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation(AliasForWithMirroredAliasForWrongAttribute.class);
|
||||
|
||||
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
|
||||
// reflection, we cannot make the test dependent on any specific ordering.
|
||||
// In other words, we can't be certain which type of exception message we'll get,
|
||||
// so we allow for both possibilities.
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(containsString("Attribute [bar] in"));
|
||||
exception.expectMessage(startsWith("Attribute [bar] in"));
|
||||
exception.expectMessage(containsString(AliasForWithMirroredAliasForWrongAttribute.class.getName()));
|
||||
exception.expectMessage(either(containsString("must be declared as an @AliasFor [foo], not [quux]")).
|
||||
or(containsString("is declared as an @AliasFor nonexistent attribute [quux]")));
|
||||
|
@ -808,13 +855,9 @@ public class AnnotationUtilsTests {
|
|||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(startsWith("Misconfigured aliases"));
|
||||
exception.expectMessage(containsString(AliasForAttributeOfDifferentType.class.getName()));
|
||||
|
||||
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
|
||||
// reflection, we cannot make the test dependent on any specific ordering.
|
||||
// In other words, we don't know if "foo" or "bar" will come first.
|
||||
exception.expectMessage(containsString("attribute [foo]"));
|
||||
exception.expectMessage(containsString("attribute [bar]"));
|
||||
exception.expectMessage(containsString("must declare the same return type"));
|
||||
exception.expectMessage(endsWith("must declare the same return type."));
|
||||
synthesizeAnnotation(annotation);
|
||||
}
|
||||
|
||||
|
@ -825,13 +868,9 @@ public class AnnotationUtilsTests {
|
|||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(startsWith("Misconfigured aliases"));
|
||||
exception.expectMessage(containsString(AliasForWithMissingDefaultValues.class.getName()));
|
||||
|
||||
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
|
||||
// reflection, we cannot make the test dependent on any specific ordering.
|
||||
// In other words, we don't know if "foo" or "bar" will come first.
|
||||
exception.expectMessage(containsString("attribute [foo]"));
|
||||
exception.expectMessage(containsString("attribute [bar]"));
|
||||
exception.expectMessage(containsString("must declare default values"));
|
||||
exception.expectMessage(containsString("attribute [foo] in annotation"));
|
||||
exception.expectMessage(containsString("attribute [bar] in annotation"));
|
||||
exception.expectMessage(endsWith("must declare default values."));
|
||||
synthesizeAnnotation(annotation);
|
||||
}
|
||||
|
||||
|
@ -842,13 +881,9 @@ public class AnnotationUtilsTests {
|
|||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(startsWith("Misconfigured aliases"));
|
||||
exception.expectMessage(containsString(AliasForAttributeWithDifferentDefaultValue.class.getName()));
|
||||
|
||||
// Since JDK 7+ does not guarantee consistent ordering of methods returned using
|
||||
// reflection, we cannot make the test dependent on any specific ordering.
|
||||
// In other words, we don't know if "foo" or "bar" will come first.
|
||||
exception.expectMessage(containsString("attribute [foo]"));
|
||||
exception.expectMessage(containsString("attribute [bar]"));
|
||||
exception.expectMessage(containsString("must declare the same default value"));
|
||||
exception.expectMessage(containsString("attribute [foo] in annotation"));
|
||||
exception.expectMessage(containsString("attribute [bar] in annotation"));
|
||||
exception.expectMessage(endsWith("must declare the same default value."));
|
||||
synthesizeAnnotation(annotation);
|
||||
}
|
||||
|
||||
|
@ -887,13 +922,91 @@ public class AnnotationUtilsTests {
|
|||
assertEquals("actual value attribute: ", "/test", synthesizedWebMapping2.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationWithImplicitAliases() throws Exception {
|
||||
assertAnnotationSynthesisWithImplicitAliases(ValueImplicitAliasesContextConfigClass.class, "value");
|
||||
assertAnnotationSynthesisWithImplicitAliases(Location1ImplicitAliasesContextConfigClass.class, "location1");
|
||||
assertAnnotationSynthesisWithImplicitAliases(XmlImplicitAliasesContextConfigClass.class, "xmlFile");
|
||||
assertAnnotationSynthesisWithImplicitAliases(GroovyImplicitAliasesContextConfigClass.class, "groovyScript");
|
||||
}
|
||||
|
||||
private void assertAnnotationSynthesisWithImplicitAliases(Class<?> clazz, String expected) throws Exception {
|
||||
ImplicitAliasesContextConfig config = clazz.getAnnotation(ImplicitAliasesContextConfig.class);
|
||||
assertNotNull(config);
|
||||
|
||||
ImplicitAliasesContextConfig synthesizedConfig = synthesizeAnnotation(config);
|
||||
assertThat(synthesizedConfig, instanceOf(SynthesizedAnnotation.class));
|
||||
assertNotSame(config, synthesizedConfig);
|
||||
|
||||
assertEquals("value: ", expected, synthesizedConfig.value());
|
||||
assertEquals("location1: ", expected, synthesizedConfig.location1());
|
||||
assertEquals("xmlFile: ", expected, synthesizedConfig.xmlFile());
|
||||
assertEquals("groovyScript: ", expected, synthesizedConfig.groovyScript());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationWithImplicitAliasesWithMissingDefaultValues() throws Exception {
|
||||
Class<?> clazz = ImplicitAliasesWithMissingDefaultValuesContextConfigClass.class;
|
||||
Class<ImplicitAliasesWithMissingDefaultValuesContextConfig> annotationType = ImplicitAliasesWithMissingDefaultValuesContextConfig.class;
|
||||
ImplicitAliasesWithMissingDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
|
||||
assertNotNull(config);
|
||||
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(startsWith("Misconfigured aliases:"));
|
||||
exception.expectMessage(containsString("attribute [location1] in annotation [" + annotationType.getName() + "]"));
|
||||
exception.expectMessage(containsString("attribute [location2] in annotation [" + annotationType.getName() + "]"));
|
||||
exception.expectMessage(endsWith("must declare default values."));
|
||||
|
||||
synthesizeAnnotation(config, clazz);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationWithImplicitAliasesWithDifferentDefaultValues() throws Exception {
|
||||
Class<?> clazz = ImplicitAliasesWithDifferentDefaultValuesContextConfigClass.class;
|
||||
Class<ImplicitAliasesWithDifferentDefaultValuesContextConfig> annotationType = ImplicitAliasesWithDifferentDefaultValuesContextConfig.class;
|
||||
ImplicitAliasesWithDifferentDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
|
||||
assertNotNull(config);
|
||||
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(startsWith("Misconfigured aliases:"));
|
||||
exception.expectMessage(containsString("attribute [location1] in annotation [" + annotationType.getName() + "]"));
|
||||
exception.expectMessage(containsString("attribute [location2] in annotation [" + annotationType.getName() + "]"));
|
||||
exception.expectMessage(endsWith("must declare the same default value."));
|
||||
|
||||
synthesizeAnnotation(config, clazz);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationWithImplicitAliasesWithDuplicateValues() throws Exception {
|
||||
Class<?> clazz = ImplicitAliasesWithDuplicateValuesContextConfigClass.class;
|
||||
Class<ImplicitAliasesWithDuplicateValuesContextConfig> annotationType = ImplicitAliasesWithDuplicateValuesContextConfig.class;
|
||||
ImplicitAliasesWithDuplicateValuesContextConfig config = clazz.getAnnotation(annotationType);
|
||||
assertNotNull(config);
|
||||
|
||||
ImplicitAliasesWithDuplicateValuesContextConfig synthesizedConfig = synthesizeAnnotation(config, clazz);
|
||||
assertNotNull(synthesizedConfig);
|
||||
|
||||
exception.expect(AnnotationConfigurationException.class);
|
||||
exception.expectMessage(startsWith("In annotation"));
|
||||
exception.expectMessage(containsString(annotationType.getName()));
|
||||
exception.expectMessage(containsString("declared on class"));
|
||||
exception.expectMessage(containsString(clazz.getName()));
|
||||
exception.expectMessage(containsString("and synthesized from"));
|
||||
exception.expectMessage(either(containsString("attribute 'location1' and its alias 'location2'")).or(
|
||||
containsString("attribute 'location2' and its alias 'location1'")));
|
||||
exception.expectMessage(either(containsString("are present with values of [1] and [2]")).or(
|
||||
containsString("are present with values of [2] and [1]")));
|
||||
exception.expectMessage(endsWith("but only one is permitted."));
|
||||
|
||||
synthesizedConfig.location1();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationFromMapWithoutAttributeAliases() throws Exception {
|
||||
Component component = WebController.class.getAnnotation(Component.class);
|
||||
assertNotNull(component);
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put(VALUE, "webController");
|
||||
Map<String, Object> map = Collections.singletonMap(VALUE, "webController");
|
||||
Component synthesizedComponent = synthesizeAnnotation(map, Component.class, WebController.class);
|
||||
assertNotNull(synthesizedComponent);
|
||||
|
||||
|
@ -979,14 +1092,35 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@Test
|
||||
public void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() throws Exception {
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("location", "test.xml");
|
||||
Map<String, Object> map = Collections.singletonMap("location", "test.xml");
|
||||
ContextConfig contextConfig = synthesizeAnnotation(map, ContextConfig.class, null);
|
||||
assertNotNull(contextConfig);
|
||||
assertEquals("value: ", "test.xml", contextConfig.value());
|
||||
assertEquals("location: ", "test.xml", contextConfig.location());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception {
|
||||
assertAnnotationSynthesisFromMapWithImplicitAliases("value");
|
||||
assertAnnotationSynthesisFromMapWithImplicitAliases("location1");
|
||||
assertAnnotationSynthesisFromMapWithImplicitAliases("location2");
|
||||
assertAnnotationSynthesisFromMapWithImplicitAliases("location3");
|
||||
assertAnnotationSynthesisFromMapWithImplicitAliases("xmlFile");
|
||||
assertAnnotationSynthesisFromMapWithImplicitAliases("groovyScript");
|
||||
}
|
||||
|
||||
private void assertAnnotationSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) throws Exception {
|
||||
Map<String, Object> map = Collections.singletonMap(attributeNameAndValue, attributeNameAndValue);
|
||||
ImplicitAliasesContextConfig config = synthesizeAnnotation(map, ImplicitAliasesContextConfig.class, null);
|
||||
assertNotNull(config);
|
||||
assertEquals("value: ", attributeNameAndValue, config.value());
|
||||
assertEquals("location1: ", attributeNameAndValue, config.location1());
|
||||
assertEquals("location2: ", attributeNameAndValue, config.location2());
|
||||
assertEquals("location3: ", attributeNameAndValue, config.location3());
|
||||
assertEquals("xmlFile: ", attributeNameAndValue, config.xmlFile());
|
||||
assertEquals("groovyScript: ", attributeNameAndValue, config.groovyScript());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationFromMapWithMissingAttributeValue() throws Exception {
|
||||
assertMissingTextAttribute(Collections.emptyMap());
|
||||
|
@ -994,8 +1128,7 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@Test
|
||||
public void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception {
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put("text", null);
|
||||
Map<String, Object> map = Collections.singletonMap("text", null);
|
||||
assertTrue(map.containsKey("text"));
|
||||
assertMissingTextAttribute(map);
|
||||
}
|
||||
|
@ -1010,8 +1143,7 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@Test
|
||||
public void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() throws Exception {
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put(VALUE, 42L);
|
||||
Map<String, Object> map = Collections.singletonMap(VALUE, 42L);
|
||||
|
||||
exception.expect(IllegalArgumentException.class);
|
||||
exception.expectMessage(startsWith("Attributes map"));
|
||||
|
@ -1183,7 +1315,7 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@Test
|
||||
public void synthesizeAnnotationWithAttributeAliasesInNestedAnnotations() throws Exception {
|
||||
List<String> expectedLocations = Arrays.asList("A", "B");
|
||||
List<String> expectedLocations = asList("A", "B");
|
||||
|
||||
Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class);
|
||||
assertNotNull(hierarchy);
|
||||
|
@ -1194,18 +1326,18 @@ public class AnnotationUtilsTests {
|
|||
ContextConfig[] configs = synthesizedHierarchy.value();
|
||||
assertNotNull(configs);
|
||||
assertTrue("nested annotations must be synthesized",
|
||||
Arrays.stream(configs).allMatch(c -> c instanceof SynthesizedAnnotation));
|
||||
stream(configs).allMatch(c -> c instanceof SynthesizedAnnotation));
|
||||
|
||||
List<String> locations = Arrays.stream(configs).map(ContextConfig::location).collect(toList());
|
||||
List<String> locations = stream(configs).map(ContextConfig::location).collect(toList());
|
||||
assertThat(locations, is(expectedLocations));
|
||||
|
||||
List<String> values = Arrays.stream(configs).map(ContextConfig::value).collect(toList());
|
||||
List<String> values = stream(configs).map(ContextConfig::value).collect(toList());
|
||||
assertThat(values, is(expectedLocations));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void synthesizeAnnotationWithArrayOfAnnotations() throws Exception {
|
||||
List<String> expectedLocations = Arrays.asList("A", "B");
|
||||
List<String> expectedLocations = asList("A", "B");
|
||||
|
||||
Hierarchy hierarchy = ConfigHierarchyTestCase.class.getAnnotation(Hierarchy.class);
|
||||
assertNotNull(hierarchy);
|
||||
|
@ -1216,7 +1348,7 @@ public class AnnotationUtilsTests {
|
|||
assertNotNull(contextConfig);
|
||||
|
||||
ContextConfig[] configs = synthesizedHierarchy.value();
|
||||
List<String> locations = Arrays.stream(configs).map(ContextConfig::location).collect(toList());
|
||||
List<String> locations = stream(configs).map(ContextConfig::location).collect(toList());
|
||||
assertThat(locations, is(expectedLocations));
|
||||
|
||||
// Alter array returned from synthesized annotation
|
||||
|
@ -1224,7 +1356,7 @@ public class AnnotationUtilsTests {
|
|||
|
||||
// Re-retrieve the array from the synthesized annotation
|
||||
configs = synthesizedHierarchy.value();
|
||||
List<String> values = Arrays.stream(configs).map(ContextConfig::value).collect(toList());
|
||||
List<String> values = stream(configs).map(ContextConfig::value).collect(toList());
|
||||
assertThat(values, is(expectedLocations));
|
||||
}
|
||||
|
||||
|
@ -1595,6 +1727,8 @@ public class AnnotationUtilsTests {
|
|||
|
||||
@AliasFor("value")
|
||||
String location() default "";
|
||||
|
||||
Class<?> klass() default Object.class;
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
@ -1770,6 +1904,109 @@ public class AnnotationUtilsTests {
|
|||
String xmlConfigFile();
|
||||
}
|
||||
|
||||
@ContextConfig
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ImplicitAliasesContextConfig {
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String xmlFile() default "";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, value = "location")
|
||||
String groovyScript() default "";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String value() default "";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location1() default "";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location2() default "";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location3() default "";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "klass")
|
||||
Class<?> configClass() default Object.class;
|
||||
|
||||
String nonAliasedAttribute() default "";
|
||||
}
|
||||
|
||||
// Attribute value intentionally matches attribute name:
|
||||
@ImplicitAliasesContextConfig(groovyScript = "groovyScript")
|
||||
static class GroovyImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
// Attribute value intentionally matches attribute name:
|
||||
@ImplicitAliasesContextConfig(xmlFile = "xmlFile")
|
||||
static class XmlImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
// Attribute value intentionally matches attribute name:
|
||||
@ImplicitAliasesContextConfig("value")
|
||||
static class ValueImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
// Attribute value intentionally matches attribute name:
|
||||
@ImplicitAliasesContextConfig(location1 = "location1")
|
||||
static class Location1ImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
// Attribute value intentionally matches attribute name:
|
||||
@ImplicitAliasesContextConfig(location2 = "location2")
|
||||
static class Location2ImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
// Attribute value intentionally matches attribute name:
|
||||
@ImplicitAliasesContextConfig(location3 = "location3")
|
||||
static class Location3ImplicitAliasesContextConfigClass {
|
||||
}
|
||||
|
||||
@ContextConfig
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ImplicitAliasesWithMissingDefaultValuesContextConfig {
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location1();
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location2();
|
||||
}
|
||||
|
||||
@ImplicitAliasesWithMissingDefaultValuesContextConfig(location1 = "1", location2 = "2")
|
||||
static class ImplicitAliasesWithMissingDefaultValuesContextConfigClass {
|
||||
}
|
||||
|
||||
@ContextConfig
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ImplicitAliasesWithDifferentDefaultValuesContextConfig {
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location1() default "foo";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location2() default "bar";
|
||||
}
|
||||
|
||||
@ImplicitAliasesWithDifferentDefaultValuesContextConfig(location1 = "1", location2 = "2")
|
||||
static class ImplicitAliasesWithDifferentDefaultValuesContextConfigClass {
|
||||
}
|
||||
|
||||
@ContextConfig
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ImplicitAliasesWithDuplicateValuesContextConfig {
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location1() default "";
|
||||
|
||||
@AliasFor(annotation = ContextConfig.class, attribute = "location")
|
||||
String location2() default "";
|
||||
}
|
||||
|
||||
@ImplicitAliasesWithDuplicateValuesContextConfig(location1 = "1", location2 = "2")
|
||||
static class ImplicitAliasesWithDuplicateValuesContextConfigClass {
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({})
|
||||
@interface Filter {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MapAnnotationAttributeExtractor}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 4.2.1
|
||||
*/
|
||||
public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase {
|
||||
|
||||
@Before
|
||||
public void clearCachesBeforeTests() {
|
||||
AnnotationUtilsTests.clearCaches();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("serial")
|
||||
public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() {
|
||||
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
|
||||
put("groovyScript", "");
|
||||
put("xmlFile", "");
|
||||
put("value", "");
|
||||
put("location1", "");
|
||||
put("location2", "");
|
||||
put("location3", "");
|
||||
put("nonAliasedAttribute", "");
|
||||
put("configClass", Object.class);
|
||||
}};
|
||||
|
||||
assertEnrichAndValidateAttributes(attributes, expectedAttributes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("serial")
|
||||
public void enrichAndValidateAttributesWithImplicitAliases() {
|
||||
Map<String, Object> attributes = new HashMap<String, Object>() {{
|
||||
put("groovyScript", "groovy!");
|
||||
}};
|
||||
|
||||
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
|
||||
put("groovyScript", "groovy!");
|
||||
put("xmlFile", "groovy!");
|
||||
put("value", "groovy!");
|
||||
put("location1", "groovy!");
|
||||
put("location2", "groovy!");
|
||||
put("location3", "groovy!");
|
||||
put("nonAliasedAttribute", "");
|
||||
put("configClass", Object.class);
|
||||
}};
|
||||
|
||||
assertEnrichAndValidateAttributes(attributes, expectedAttributes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void assertEnrichAndValidateAttributes(Map<String, Object> sourceAttributes, Map<String, Object> expected) {
|
||||
Class<? extends Annotation> annotationType = ImplicitAliasesContextConfig.class;
|
||||
|
||||
// Since the ordering of attribute methods returned by the JVM is
|
||||
// non-deterministic, we have to rig the attributeAliasesCache in AnnotationUtils
|
||||
// so that the tests consistently fail in case enrichAndValidateAttributes() is
|
||||
// buggy.
|
||||
//
|
||||
// Otherwise, these tests would intermittently pass even for an invalid
|
||||
// implementation.
|
||||
Map<Class<? extends Annotation>, MultiValueMap<String, String>> attributeAliasesCache =
|
||||
(Map<Class<? extends Annotation>, MultiValueMap<String, String>>) AnnotationUtilsTests.getCache("attributeAliasesCache");
|
||||
|
||||
// Declare aliases in an order that will cause enrichAndValidateAttributes() to
|
||||
// fail unless it considers all aliases in the set of implicit aliases.
|
||||
MultiValueMap<String, String> aliases = new LinkedMultiValueMap<String, String>();
|
||||
aliases.put("xmlFile", Arrays.asList("value", "groovyScript", "location1", "location2", "location3"));
|
||||
aliases.put("groovyScript", Arrays.asList("value", "xmlFile", "location1", "location2", "location3"));
|
||||
aliases.put("value", Arrays.asList("xmlFile", "groovyScript", "location1", "location2", "location3"));
|
||||
aliases.put("location1", Arrays.asList("xmlFile", "groovyScript", "value", "location2", "location3"));
|
||||
aliases.put("location2", Arrays.asList("xmlFile", "groovyScript", "value", "location1", "location3"));
|
||||
aliases.put("location3", Arrays.asList("xmlFile", "groovyScript", "value", "location1", "location2"));
|
||||
|
||||
attributeAliasesCache.put(annotationType, aliases);
|
||||
|
||||
MapAnnotationAttributeExtractor extractor = new MapAnnotationAttributeExtractor(sourceAttributes, annotationType, null);
|
||||
Map<String, Object> enriched = extractor.getSource();
|
||||
|
||||
assertEquals("attribute map size", expected.size(), enriched.size());
|
||||
expected.keySet().stream().forEach( attr ->
|
||||
assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expected.get(attr))));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AnnotationAttributeExtractor<?> createExtractorFor(Class<?> clazz, String expected, Class<? extends Annotation> annotationType) {
|
||||
Map<String, Object> attributes = Collections.singletonMap(expected, expected);
|
||||
return new MapAnnotationAttributeExtractor(attributes, annotationType, clazz);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue