diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index 0b39516182..a284755161 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -956,21 +956,14 @@ public class AnnotatedElementUtils { for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) { String attributeName = attributeMethod.getName(); - List aliases = AnnotationUtils.getAliasedAttributeNames(attributeMethod, targetAnnotationType); + String attributeOverrideName = AnnotationUtils.getAttributeOverrideName(attributeMethod, targetAnnotationType); // Explicit annotation attribute override declared via @AliasFor - 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); + if (attributeOverrideName != null) { + if (attributes.containsKey(attributeOverrideName)) { + overrideAttribute(element, annotation, attributes, attributeName, attributeOverrideName); } } - // Implicit annotation attribute override based on convention else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) { overrideAttribute(element, annotation, attributes, attributeName, attributeName); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 3ca49bcbce..d758d28bdf 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -137,6 +137,9 @@ public abstract class AnnotationUtils { private static final Map, List> attributeMethodsCache = new ConcurrentReferenceHashMap, List>(256); + private static final Map aliasDescriptorCache = + new ConcurrentReferenceHashMap(256); + private static transient Log logger; @@ -1436,7 +1439,7 @@ public abstract class AnnotationUtils { map = new HashMap>(); for (Method attribute : getAttributeMethods(annotationType)) { - List aliasNames = getAliasedAttributeNames(attribute); + List aliasNames = getAttributeAliasNames(attribute); if (!aliasNames.isEmpty()) { map.put(attribute.getName(), aliasNames); } @@ -1469,7 +1472,7 @@ public abstract class AnnotationUtils { synthesizable = Boolean.FALSE; for (Method attribute : getAttributeMethods(annotationType)) { - if (!getAliasedAttributeNames(attribute).isEmpty()) { + if (!getAttributeAliasNames(attribute).isEmpty()) { synthesizable = Boolean.TRUE; break; } @@ -1497,7 +1500,6 @@ public abstract class AnnotationUtils { /** * Get the names of the aliased attributes configured via * {@link AliasFor @AliasFor} for the supplied annotation {@code attribute}. - *

This method does not resolve meta-annotation attribute overrides. * @param attribute the attribute to find aliases for; never {@code null} * @return the names of the aliased attributes; never {@code null}, though * potentially empty @@ -1506,74 +1508,39 @@ public abstract class AnnotationUtils { * @throws AnnotationConfigurationException if invalid configuration of * {@code @AliasFor} is detected * @since 4.2 - * @see #getAliasedAttributeNames(Method, Class) + * @see #getAttributeOverrideName(Method, Class) */ - static List getAliasedAttributeNames(Method attribute) { - return getAliasedAttributeNames(attribute, (Class) null); + static List getAttributeAliasNames(Method attribute) { + Assert.notNull(attribute, "attribute must not be null"); + + AliasDescriptor descriptor = AliasDescriptor.from(attribute); + return (descriptor == null ? Collections.emptyList() : descriptor.getAttributeAliasNames()); } /** - * Get the names of the aliased attributes configured via + * Get the name of the overridden attribute configured via * {@link AliasFor @AliasFor} for the supplied annotation {@code attribute}. - *

If the supplied {@code metaAnnotationType} is non-null, the - * returned list will contain at most one element. - * @param attribute the attribute to find aliases for; never {@code null} - * @param metaAnnotationType the type of meta-annotation in which an - * aliased attribute is allowed to be declared; {@code null} implies - * within the same annotation as the supplied attribute - * @return the names of the aliased attributes; never {@code null}, though - * potentially empty + * @param attribute the attribute from which to retrieve the override; + * never {@code null} + * @param metaAnnotationType the type of meta-annotation in which the + * overridden attribute is allowed to be declared + * @return the name of the overridden attribute, or {@code null} if not + * found or not applicable for the specified meta-annotation type * @throws IllegalArgumentException if the supplied attribute method is * {@code null} or not from an annotation, or if the supplied meta-annotation - * type is {@link Annotation} + * type is {@code null} or {@link Annotation} * @throws AnnotationConfigurationException if invalid configuration of * {@code @AliasFor} is detected * @since 4.2 */ - static List getAliasedAttributeNames(Method attribute, Class metaAnnotationType) { - Assert.notNull(attribute, "attribute method must not be null"); + static String getAttributeOverrideName(Method attribute, Class metaAnnotationType) { + Assert.notNull(attribute, "attribute must not be null"); + Assert.notNull(metaAnnotationType, "metaAnnotationType must not be null"); Assert.isTrue(!Annotation.class.equals(metaAnnotationType), "metaAnnotationType must not be java.lang.annotation.Annotation"); AliasDescriptor descriptor = AliasDescriptor.from(attribute); - - // No alias declared via @AliasFor? - if (descriptor == null) { - return Collections.emptyList(); - } - - // Searching for explicit meta-annotation attribute override? - if (metaAnnotationType != null) { - if (descriptor.isAliasFor(metaAnnotationType)) { - return Collections.singletonList(descriptor.aliasedAttributeName); - } - // Else: explicit attribute override for a different meta-annotation - return Collections.emptyList(); - } - - // Explicit alias pair? - if (descriptor.isAliasPair) { - return Collections.singletonList(descriptor.aliasedAttributeName); - } - - // Else: search for implicit aliases - List aliases = new ArrayList(); - for (Method currentAttribute : getAttributeMethods(descriptor.sourceAnnotationType)) { - - // An attribute cannot alias itself - if (attribute.equals(currentAttribute)) { - continue; - } - - // If two attributes override the same attribute in the same meta-annotation, - // they are "implicit" aliases for each other. - AliasDescriptor otherDescriptor = AliasDescriptor.from(currentAttribute); - if (descriptor.equals(otherDescriptor)) { - descriptor.validateAgainst(otherDescriptor); - aliases.add(otherDescriptor.sourceAttributeName); - } - } - return aliases; + return (descriptor == null ? null : descriptor.getAttributeOverrideName(metaAnnotationType)); } /** @@ -1912,6 +1879,9 @@ public abstract class AnnotationUtils { * on a given annotation attribute and includes support for validating * the configuration of aliases (both explicit and implicit). * @since 4.2.1 + * @see #from + * @see #getAttributeAliasNames + * @see #getAttributeOverrideName */ private static class AliasDescriptor { @@ -1921,6 +1891,8 @@ public abstract class AnnotationUtils { private final String sourceAttributeName; + private final Method aliasedAttribute; + private final Class aliasedAnnotationType; private final String aliasedAttributeName; @@ -1929,37 +1901,55 @@ public abstract class AnnotationUtils { /** - * Create a new {@code AliasDescriptor} from the declaration + * Create an {@code AliasDescriptor} from the declaration * of {@code @AliasFor} on the supplied annotation attribute and * validate the configuration of {@code @AliasFor}. * @param attribute the annotation attribute that is annotated with * {@code @AliasFor} - * @return a new alias descriptor, or {@code null} if the attribute + * @return an alias descriptor, or {@code null} if the attribute * is not annotated with {@code @AliasFor} - * @see #validateAgainst(AliasDescriptor) + * @see #validateAgainst */ public static AliasDescriptor from(Method attribute) { + AliasDescriptor descriptor = aliasDescriptorCache.get(attribute); + if (descriptor != null) { + return descriptor; + } + AliasFor aliasFor = attribute.getAnnotation(AliasFor.class); if (aliasFor == null) { return null; } - AliasDescriptor descriptor = new AliasDescriptor(attribute, aliasFor); + descriptor = new AliasDescriptor(attribute, aliasFor); descriptor.validate(); + aliasDescriptorCache.put(attribute, descriptor); 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"); + Assert.isTrue(declaringClass.isAnnotation(), "sourceAttribute must be from an annotation"); this.sourceAttribute = sourceAttribute; this.sourceAnnotationType = (Class) declaringClass; this.sourceAttributeName = this.sourceAttribute.getName(); + this.aliasedAnnotationType = (Annotation.class.equals(aliasFor.annotation()) ? this.sourceAnnotationType : aliasFor.annotation()); this.aliasedAttributeName = getAliasedAttributeName(aliasFor, this.sourceAttribute); + try { + this.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); + } + this.isAliasPair = this.sourceAnnotationType.equals(this.aliasedAnnotationType); } @@ -1974,20 +1964,8 @@ public abstract class AnnotationUtils { 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); + AliasFor mirrorAliasFor = this.aliasedAttribute.getAnnotation(AliasFor.class); if (mirrorAliasFor == null) { String msg = String.format( "Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s].", @@ -1995,8 +1973,7 @@ public abstract class AnnotationUtils { throw new AnnotationConfigurationException(msg); } - String mirrorAliasedAttributeName = getAliasedAttributeName(mirrorAliasFor, - aliasedAttribute); + String mirrorAliasedAttributeName = getAliasedAttributeName(mirrorAliasFor, this.aliasedAttribute); if (!this.sourceAttributeName.equals(mirrorAliasedAttributeName)) { String msg = String.format( "Attribute [%s] in annotation [%s] must be declared as an @AliasFor [%s], not [%s].", @@ -2007,7 +1984,7 @@ public abstract class AnnotationUtils { } Class returnType = this.sourceAttribute.getReturnType(); - Class aliasedReturnType = aliasedAttribute.getReturnType(); + Class aliasedReturnType = this.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.", @@ -2017,7 +1994,7 @@ public abstract class AnnotationUtils { } if (this.isAliasPair) { - validateDefaultValueConfiguration(aliasedAttribute); + validateDefaultValueConfiguration(this.aliasedAttribute); } } @@ -2047,63 +2024,101 @@ public abstract class AnnotationUtils { * Validate this descriptor against the supplied descriptor. *

This method only validates the configuration of default values * for the two descriptors, since other aspects of the descriptors - * were validated when the descriptors were created. + * are validated when they are created. */ - public void validateAgainst(AliasDescriptor otherDescriptor) { + private void validateAgainst(AliasDescriptor otherDescriptor) { validateDefaultValueConfiguration(otherDescriptor.sourceAttribute); } /** - * Does this descriptor represent an alias for an attribute in the - * supplied {@code targetAnnotationType}? + * Determine if this descriptor represents an explicit override for + * an attribute in the supplied {@code metaAnnotationType}. + * @see #isAliasFor */ - public boolean isAliasFor(Class targetAnnotationType) { - return targetAnnotationType.equals(this.aliasedAnnotationType); + private boolean isOverrideFor(Class metaAnnotationType) { + return this.aliasedAnnotationType.equals(metaAnnotationType); } /** - * Determine if this descriptor is logically equal to the supplied - * object. - *

Two descriptors are considered equal if the aliases they - * represent are from attributes in one annotation that alias the - * same attribute in a given target annotation. + * Determine if this descriptor and the supplied descriptor both + * effectively represent aliases for the same attribute in the same + * target annotation, either explicitly or implicitly. + *

This method searches the attribute override hierarchy, beginning + * with this descriptor, in order to detect implicit and transitively + * implicit aliases. + * @return {@code true} if this descriptor and the supplied descriptor + * effectively alias the same annotation attribute + * @see #isOverrideFor */ - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof AliasDescriptor)) { - return false; + private boolean isAliasFor(AliasDescriptor otherDescriptor) { + for (AliasDescriptor lhs = this; lhs != null; lhs = lhs.getAttributeOverrideDescriptor()) { + for (AliasDescriptor rhs = otherDescriptor; rhs != null; rhs = rhs.getAttributeOverrideDescriptor()) { + if (lhs.aliasedAttribute.equals(rhs.aliasedAttribute)) { + return true; + } + } } + return false; + } - AliasDescriptor that = (AliasDescriptor) other; + public List getAttributeAliasNames() { + // Explicit alias pair? + if (this.isAliasPair) { + return Collections.singletonList(this.aliasedAttributeName); + } - if (!this.sourceAnnotationType.equals(that.sourceAnnotationType)) { - return false; + // Else: search for implicit aliases + List aliases = new ArrayList(); + for (AliasDescriptor otherDescriptor : getOtherDescriptors()) { + if (this.isAliasFor(otherDescriptor)) { + this.validateAgainst(otherDescriptor); + aliases.add(otherDescriptor.sourceAttributeName); + } } - if (!this.aliasedAnnotationType.equals(that.aliasedAnnotationType)) { - return false; + return aliases; + } + + private List getOtherDescriptors() { + List otherDescriptors = new ArrayList(); + for (Method currentAttribute : getAttributeMethods(this.sourceAnnotationType)) { + if (!this.sourceAttribute.equals(currentAttribute)) { + AliasDescriptor otherDescriptor = AliasDescriptor.from(currentAttribute); + if (otherDescriptor != null) { + otherDescriptors.add(otherDescriptor); + } + } } - if (!this.aliasedAttributeName.equals(that.aliasedAttributeName)) { - return false; + return otherDescriptors; + } + + public String getAttributeOverrideName(Class metaAnnotationType) { + Assert.notNull(metaAnnotationType, "metaAnnotationType must not be null"); + Assert.isTrue(!Annotation.class.equals(metaAnnotationType), + "metaAnnotationType must not be java.lang.annotation.Annotation"); + + // Search the attribute override hierarchy, starting with the current attribute + for (AliasDescriptor desc = this; desc != null; desc = desc.getAttributeOverrideDescriptor()) { + if (desc.isOverrideFor(metaAnnotationType)) { + return desc.aliasedAttributeName; + } } - return true; + // Else: explicit attribute override for a different meta-annotation + return null; } - @Override - public int hashCode() { - int result = this.sourceAnnotationType.hashCode(); - result = 31 * result + this.aliasedAnnotationType.hashCode(); - result = 31 * result + this.aliasedAttributeName.hashCode(); - return result; + private AliasDescriptor getAttributeOverrideDescriptor() { + if (this.isAliasPair) { + return null; + } + return AliasDescriptor.from(this.aliasedAttribute); } @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)); + return String.format("%s: @%s(%s) is an alias for @%s(%s)", getClass().getSimpleName(), + this.sourceAnnotationType.getSimpleName(), this.sourceAttributeName, + this.aliasedAnnotationType.getSimpleName(), this.aliasedAttributeName); } /** @@ -2121,7 +2136,6 @@ public abstract class AnnotationUtils { * @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(); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index feccac0c1d..3b2b7a59c8 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -351,13 +351,23 @@ public class AnnotatedElementUtilsTests { assertGetMergedAnnotation(ImplicitAliasesContextConfigClass3.class, "baz.xml"); } - private void assertGetMergedAnnotation(Class element, String expected) { + @Test + public void getMergedAnnotationWithTransitiveImplicitAliases() { + assertGetMergedAnnotation(TransitiveImplicitAliasesContextConfigClass.class, "test.groovy"); + } + + @Test + public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevel() { + assertGetMergedAnnotation(TransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.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[] { expected }, contextConfig.locations()); - assertArrayEquals("value", new String[] { expected }, contextConfig.value()); + assertArrayEquals("locations", expected, contextConfig.locations()); + assertArrayEquals("value", expected, contextConfig.value()); assertArrayEquals("classes", new Class[0], contextConfig.classes()); // Verify contracts between utility methods: @@ -800,6 +810,28 @@ public class AnnotatedElementUtilsTests { @interface ComposedImplicitAliasesContextConfig { } + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesContextConfig { + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "xmlFiles") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesWithSkippedLevelContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + /** * Invalid because the configuration declares a value for 'value' and * requires a value for the aliased 'locations'. So we likely end up with @@ -1028,6 +1060,14 @@ public class AnnotatedElementUtilsTests { static class ImplicitAliasesContextConfigClass3 { } + @TransitiveImplicitAliasesContextConfig(groovy = "test.groovy") + static class TransitiveImplicitAliasesContextConfigClass { + } + + @TransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml") + static class TransitiveImplicitAliasesWithSkippedLevelContextConfigClass { + } + @ComposedImplicitAliasesContextConfig static class ComposedImplicitAliasesContextConfigClass { } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index 77e4e69bbc..2b6caa0044 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -59,7 +59,7 @@ public class AnnotationUtilsTests { static void clearCaches() { clearCache("findAnnotationCache", "annotatedInterfaceCache", "metaPresentCache", "synthesizableCache", - "attributeAliasesCache", "attributeMethodsCache"); + "attributeAliasesCache", "attributeMethodsCache", "aliasDescriptorCache"); } static void clearCache(String... cacheNames) { @@ -720,26 +720,26 @@ public class AnnotationUtilsTests { } @Test - public void getAliasedAttributeNamesFromWrongTargetAnnotation() throws Exception { + public void getAttributeOverrideNameFromWrongTargetAnnotation() throws Exception { Method attribute = AliasedComposedContextConfig.class.getDeclaredMethod("xmlConfigFile"); assertThat("xmlConfigFile is not an alias for @Component.", - getAliasedAttributeNames(attribute, Component.class), is(empty())); + getAttributeOverrideName(attribute, Component.class), is(nullValue())); } @Test - public void getAliasedAttributeNamesForNonAliasedAttribute() throws Exception { + public void getAttributeOverrideNameForNonAliasedAttribute() throws Exception { Method nonAliasedAttribute = ImplicitAliasesContextConfig.class.getDeclaredMethod("nonAliasedAttribute"); - assertThat(getAliasedAttributeNames(nonAliasedAttribute, ContextConfig.class), is(empty())); + assertThat(getAttributeOverrideName(nonAliasedAttribute, ContextConfig.class), is(nullValue())); } @Test - public void getAliasedAttributeNamesFromAliasedComposedAnnotation() throws Exception { + public void getAttributeOverrideNameFromAliasedComposedAnnotation() throws Exception { Method attribute = AliasedComposedContextConfig.class.getDeclaredMethod("xmlConfigFile"); - assertEquals(asList("location"), getAliasedAttributeNames(attribute, ContextConfig.class)); + assertEquals("location", getAttributeOverrideName(attribute, ContextConfig.class)); } @Test - public void getAliasedAttributeNamesFromComposedAnnotationWithImplicitAliases() throws Exception { + public void getAttributeAliasNamesFromComposedAnnotationWithImplicitAliases() throws Exception { Method xmlFile = ImplicitAliasesContextConfig.class.getDeclaredMethod("xmlFile"); Method groovyScript = ImplicitAliasesContextConfig.class.getDeclaredMethod("groovyScript"); Method value = ImplicitAliasesContextConfig.class.getDeclaredMethod("value"); @@ -748,17 +748,63 @@ public class AnnotationUtilsTests { 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)); + assertEquals("location", getAttributeOverrideName(xmlFile, ContextConfig.class)); + assertEquals("location", getAttributeOverrideName(groovyScript, ContextConfig.class)); + assertEquals("location", getAttributeOverrideName(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")); + // Implicit aliases + assertThat(getAttributeAliasNames(xmlFile), containsInAnyOrder("value", "groovyScript", "location1", "location2", "location3")); + assertThat(getAttributeAliasNames(groovyScript), containsInAnyOrder("value", "xmlFile", "location1", "location2", "location3")); + assertThat(getAttributeAliasNames(value), containsInAnyOrder("xmlFile", "groovyScript", "location1", "location2", "location3")); + assertThat(getAttributeAliasNames(location1), containsInAnyOrder("xmlFile", "groovyScript", "value", "location2", "location3")); + assertThat(getAttributeAliasNames(location2), containsInAnyOrder("xmlFile", "groovyScript", "value", "location1", "location3")); + assertThat(getAttributeAliasNames(location3), containsInAnyOrder("xmlFile", "groovyScript", "value", "location1", "location2")); + } + + @Test + public void getAttributeAliasNamesFromComposedAnnotationWithImplicitAliasesForAliasPair() throws Exception { + Method xmlFile = ImplicitAliasesForAliasPairContextConfig.class.getDeclaredMethod("xmlFile"); + Method groovyScript = ImplicitAliasesForAliasPairContextConfig.class.getDeclaredMethod("groovyScript"); + + // Meta-annotation attribute overrides + assertEquals("location", getAttributeOverrideName(xmlFile, ContextConfig.class)); + assertEquals("value", getAttributeOverrideName(groovyScript, ContextConfig.class)); + + // Implicit aliases + assertThat(getAttributeAliasNames(xmlFile), containsInAnyOrder("groovyScript")); + assertThat(getAttributeAliasNames(groovyScript), containsInAnyOrder("xmlFile")); + } + + @Test + public void getAttributeAliasNamesFromComposedAnnotationWithTransitiveImplicitAliases() throws Exception { + Method xml = TransitiveImplicitAliasesContextConfig.class.getDeclaredMethod("xml"); + Method groovy = TransitiveImplicitAliasesContextConfig.class.getDeclaredMethod("groovy"); + + // Explicit meta-annotation attribute overrides + assertEquals("xmlFile", getAttributeOverrideName(xml, ImplicitAliasesContextConfig.class)); + assertEquals("groovyScript", getAttributeOverrideName(groovy, ImplicitAliasesContextConfig.class)); + + // Transitive meta-annotation attribute overrides + assertEquals("location", getAttributeOverrideName(xml, ContextConfig.class)); + assertEquals("location", getAttributeOverrideName(groovy, ContextConfig.class)); + + // Transitive implicit aliases + assertThat(getAttributeAliasNames(xml), containsInAnyOrder("groovy")); + assertThat(getAttributeAliasNames(groovy), containsInAnyOrder("xml")); + } + + @Test + public void getAttributeAliasNamesFromComposedAnnotationWithTransitiveImplicitAliasesForAliasPair() throws Exception { + Method xml = TransitiveImplicitAliasesForAliasPairContextConfig.class.getDeclaredMethod("xml"); + Method groovy = TransitiveImplicitAliasesForAliasPairContextConfig.class.getDeclaredMethod("groovy"); + + // Explicit meta-annotation attribute overrides + assertEquals("xmlFile", getAttributeOverrideName(xml, ImplicitAliasesForAliasPairContextConfig.class)); + assertEquals("groovyScript", getAttributeOverrideName(groovy, ImplicitAliasesForAliasPairContextConfig.class)); + + // Transitive implicit aliases + assertThat(getAttributeAliasNames(xml), containsInAnyOrder("groovy")); + assertThat(getAttributeAliasNames(groovy), containsInAnyOrder("xml")); } @Test @@ -936,7 +982,6 @@ public class AnnotationUtilsTests { ImplicitAliasesContextConfig synthesizedConfig = synthesizeAnnotation(config); assertThat(synthesizedConfig, instanceOf(SynthesizedAnnotation.class)); - assertNotSame(config, synthesizedConfig); assertEquals("value: ", expected, synthesizedConfig.value()); assertEquals("location1: ", expected, synthesizedConfig.location1()); @@ -944,6 +989,45 @@ public class AnnotationUtilsTests { assertEquals("groovyScript: ", expected, synthesizedConfig.groovyScript()); } + @Test + public void synthesizeAnnotationWithImplicitAliasesForAliasPair() throws Exception { + Class clazz = ImplicitAliasesForAliasPairContextConfigClass.class; + ImplicitAliasesForAliasPairContextConfig config = clazz.getAnnotation(ImplicitAliasesForAliasPairContextConfig.class); + assertNotNull(config); + + ImplicitAliasesForAliasPairContextConfig synthesizedConfig = synthesizeAnnotation(config); + assertThat(synthesizedConfig, instanceOf(SynthesizedAnnotation.class)); + + assertEquals("xmlFile: ", "test.xml", synthesizedConfig.xmlFile()); + assertEquals("groovyScript: ", "test.xml", synthesizedConfig.groovyScript()); + } + + @Test + public void synthesizeAnnotationWithTransitiveImplicitAliases() throws Exception { + Class clazz = TransitiveImplicitAliasesContextConfigClass.class; + TransitiveImplicitAliasesContextConfig config = clazz.getAnnotation(TransitiveImplicitAliasesContextConfig.class); + assertNotNull(config); + + TransitiveImplicitAliasesContextConfig synthesizedConfig = synthesizeAnnotation(config); + assertThat(synthesizedConfig, instanceOf(SynthesizedAnnotation.class)); + + assertEquals("xml: ", "test.xml", synthesizedConfig.xml()); + assertEquals("groovy: ", "test.xml", synthesizedConfig.groovy()); + } + + @Test + public void synthesizeAnnotationWithTransitiveImplicitAliasesForAliasPair() throws Exception { + Class clazz = TransitiveImplicitAliasesForAliasPairContextConfigClass.class; + TransitiveImplicitAliasesForAliasPairContextConfig config = clazz.getAnnotation(TransitiveImplicitAliasesForAliasPairContextConfig.class); + assertNotNull(config); + + TransitiveImplicitAliasesForAliasPairContextConfig synthesizedConfig = synthesizeAnnotation(config); + assertThat(synthesizedConfig, instanceOf(SynthesizedAnnotation.class)); + + assertEquals("xml: ", "test.xml", synthesizedConfig.xml()); + assertEquals("groovy: ", "test.xml", synthesizedConfig.groovy()); + } + @Test public void synthesizeAnnotationWithImplicitAliasesWithMissingDefaultValues() throws Exception { Class clazz = ImplicitAliasesWithMissingDefaultValuesContextConfigClass.class; @@ -2007,6 +2091,51 @@ public class AnnotationUtilsTests { static class ImplicitAliasesWithDuplicateValuesContextConfigClass { } + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesForAliasPairContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String xmlFile() default ""; + + @AliasFor(annotation = ContextConfig.class, value = "value") + String groovyScript() default ""; + } + + @ImplicitAliasesForAliasPairContextConfig(xmlFile = "test.xml") + static class ImplicitAliasesForAliasPairContextConfigClass { + } + + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesContextConfig { + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesContextConfig(xml = "test.xml") + static class TransitiveImplicitAliasesContextConfigClass { + } + + @ImplicitAliasesForAliasPairContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesForAliasPairContextConfig { + + @AliasFor(annotation = ImplicitAliasesForAliasPairContextConfig.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesForAliasPairContextConfig.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesForAliasPairContextConfig(xml = "test.xml") + static class TransitiveImplicitAliasesForAliasPairContextConfigClass { + } + @Retention(RetentionPolicy.RUNTIME) @Target({}) @interface Filter {