Support implicit attribute aliases with @AliasFor

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

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

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

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

- Introduced isAnnotationMetaPresent() in AnnotationUtils.

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

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

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

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

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

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

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

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

View File

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

View File

@@ -323,16 +323,62 @@ public class AnnotatedElementUtilsTests {
}
@Test
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 {
}

View File

@@ -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}.
*/

View File

@@ -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 {

View File

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

View File

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