Allow single element to override array in synthesized annotation

This commit picks up where 8ff9e818a5
left off.

Specifically, this commit introduces support that allows a single
element attribute to override an array attribute with a matching
component type when synthesizing annotations (e.g., in annotations
synthesized from attributes that have been merged from the annotation
hierarchy above a composed annotation).

Issue: SPR-13972
This commit is contained in:
Sam Brannen
2016-03-07 13:27:27 +01:00
parent e086a5d58b
commit eb654dc177
4 changed files with 259 additions and 74 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -132,8 +133,16 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib
if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) { if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) {
boolean converted = false; boolean converted = false;
// Single element overriding an array of the same type?
if (requiredReturnType.isArray() && requiredReturnType.getComponentType() == actualReturnType) {
Object array = Array.newInstance(requiredReturnType.getComponentType(), 1);
Array.set(array, 0, attributeValue);
attributes.put(attributeName, array);
converted = true;
}
// Nested map representing a single annotation? // Nested map representing a single annotation?
if (Annotation.class.isAssignableFrom(requiredReturnType) && else if (Annotation.class.isAssignableFrom(requiredReturnType) &&
Map.class.isAssignableFrom(actualReturnType)) { Map.class.isAssignableFrom(actualReturnType)) {
Class<? extends Annotation> nestedAnnotationType = Class<? extends Annotation> nestedAnnotationType =
(Class<? extends Annotation>) requiredReturnType; (Class<? extends Annotation>) requiredReturnType;

View File

@@ -33,8 +33,11 @@ import javax.annotation.Resource;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.internal.ArrayComparisonFailure;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.core.annotation.AnnotationUtilsTests.WebController;
import org.springframework.core.annotation.AnnotationUtilsTests.WebMapping;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@@ -44,6 +47,7 @@ import static java.util.stream.Collectors.*;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotatedElementUtils.*; import static org.springframework.core.annotation.AnnotatedElementUtils.*;
import static org.springframework.core.annotation.AnnotationUtilsTests.*;
/** /**
* Unit tests for {@link AnnotatedElementUtils}. * Unit tests for {@link AnnotatedElementUtils}.
@@ -379,11 +383,21 @@ public class AnnotatedElementUtilsTests {
assertGetMergedAnnotation(TransitiveImplicitAliasesContextConfigClass.class, "test.groovy"); assertGetMergedAnnotation(TransitiveImplicitAliasesContextConfigClass.class, "test.groovy");
} }
@Test
public void getMergedAnnotationWithTransitiveImplicitAliasesWithSingleElementOverridingAnArrayViaAliasFor() {
assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesContextConfigClass.class, "test.groovy");
}
@Test @Test
public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevel() { public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevel() {
assertGetMergedAnnotation(TransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml"); assertGetMergedAnnotation(TransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml");
} }
@Test
public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevelWithSingleElementOverridingAnArrayViaAliasFor() {
assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml");
}
private void assertGetMergedAnnotation(Class<?> element, String... expected) { private void assertGetMergedAnnotation(Class<?> element, String... expected) {
String name = ContextConfig.class.getName(); String name = ContextConfig.class.getName();
ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class); ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class);
@@ -560,6 +574,53 @@ public class AnnotatedElementUtilsTests {
assertEquals("TX qualifier via synthesized annotation.", qualifier, annotation.qualifier()); assertEquals("TX qualifier via synthesized annotation.", qualifier, annotation.qualifier());
} }
@Test
public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
AnnotationAttributes attributes = assertComponentScanAttributes(TestComponentScanClass.class,
"com.example.app.test");
Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class);
assertNotNull(excludeFilters);
List<String> patterns = stream(excludeFilters).map(Filter::pattern).collect(toList());
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() {
assertComponentScanAttributes(ComponentScanWithBasePackagesAndValueAliasClass.class, "com.example.app.test");
}
@Test
public void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() {
assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test");
}
@Test
public void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaAliasFor() {
assertComponentScanAttributes(AliasForBasedSinglePackageComponentScanClass.class, "com.example.app.test");
}
private AnnotationAttributes assertComponentScanAttributes(Class<?> element, String... expected) {
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"));
return attributes;
}
private AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "annotationType must not be null");
return AnnotatedElementUtils.findMergedAnnotationAttributes(element, annotationType.getName(), false, false);
}
@Test @Test
public void findMergedAnnotationWithAttributeAliasesInTargetAnnotation() { public void findMergedAnnotationWithAttributeAliasesInTargetAnnotation() {
Class<?> element = AliasedTransactionalComponentClass.class; Class<?> element = AliasedTransactionalComponentClass.class;
@@ -593,38 +654,6 @@ public class AnnotatedElementUtilsTests {
assertArrayEquals("value", propFiles, testPropSource.value()); assertArrayEquals("value", propFiles, testPropSource.value());
} }
@Test
public void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() {
String[] expected = asArray("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, expected, attributes.getStringArray("basePackages"));
Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class);
assertNotNull(excludeFilters);
List<String> patterns = stream(excludeFilters).map(Filter::pattern).collect(toList());
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 = asArray("com.example.app.test");
Class<?> element = ComponentScanWithBasePackagesAndValueAliasClass.class;
AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class);
assertNotNull("Should find @ComponentScan on " + element, attributes);
assertArrayEquals("value: ", expected, attributes.getStringArray("value"));
assertArrayEquals("basePackages: ", expected, attributes.getStringArray("basePackages"));
}
@Test @Test
public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() {
final String[] EMPTY = new String[0]; final String[] EMPTY = new String[0];
@@ -638,6 +667,24 @@ public class AnnotatedElementUtilsTests {
assertArrayEquals("classes for " + element, new Class<?>[] {Number.class}, contextConfig.classes()); assertArrayEquals("classes for " + element, new Class<?>[] {Number.class}, contextConfig.classes());
} }
@Test
public void findMergedAnnotationWithSingleElementOverridingAnArrayViaConvention() throws Exception {
assertWebMapping(WebController.class.getMethod("postMappedWithPathAttribute"));
}
@Test
public void findMergedAnnotationWithSingleElementOverridingAnArrayViaAliasFor() throws Exception {
assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"));
assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute"));
}
private void assertWebMapping(AnnotatedElement element) throws ArrayComparisonFailure {
WebMapping webMapping = findMergedAnnotation(element, WebMapping.class);
assertNotNull(webMapping);
assertArrayEquals("value attribute: ", asArray("/test"), webMapping.value());
assertArrayEquals("path attribute: ", asArray("/test"), webMapping.path());
}
@Test @Test
public void javaLangAnnotationTypeViaFindMergedAnnotation() throws Exception { public void javaLangAnnotationTypeViaFindMergedAnnotation() throws Exception {
Constructor<?> deprecatedCtor = Date.class.getConstructor(String.class); Constructor<?> deprecatedCtor = Date.class.getConstructor(String.class);
@@ -651,28 +698,10 @@ public class AnnotatedElementUtilsTests {
assertEquals(SpringAppConfigClass.class.getAnnotation(Resource.class), findMergedAnnotation(SpringAppConfigClass.class, Resource.class)); assertEquals(SpringAppConfigClass.class.getAnnotation(Resource.class), findMergedAnnotation(SpringAppConfigClass.class, Resource.class));
} }
private Set<String> names(Class<?>... classes) { private Set<String> names(Class<?>... classes) {
return stream(classes).map(Class::getName).collect(toSet()); return stream(classes).map(Class::getName).collect(toSet());
} }
@SafeVarargs
// The following "varargs" suppression is necessary for javac from OpenJDK
// (1.8.0_60-b27); however, Eclipse warns that it's unnecessary. See the following
// Eclipse issues for details.
//
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=344783
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=349669#c10
// @SuppressWarnings("varargs")
private static <T> T[] asArray(T... arr) {
return arr;
}
private static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "annotationType must not be null");
return AnnotatedElementUtils.findMergedAnnotationAttributes(element, annotationType.getName(), false, false);
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -881,6 +910,17 @@ public class AnnotatedElementUtilsTests {
String[] groovy() default {}; String[] groovy() default {};
} }
@ImplicitAliasesContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface SingleLocationTransitiveImplicitAliasesContextConfig {
@AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "xmlFiles")
String xml() default "";
@AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts")
String groovy() default "";
}
@ImplicitAliasesContextConfig @ImplicitAliasesContextConfig
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface TransitiveImplicitAliasesWithSkippedLevelContextConfig { @interface TransitiveImplicitAliasesWithSkippedLevelContextConfig {
@@ -892,6 +932,17 @@ public class AnnotatedElementUtilsTests {
String[] groovy() default {}; String[] groovy() default {};
} }
@ImplicitAliasesContextConfig
@Retention(RetentionPolicy.RUNTIME)
@interface SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfig {
@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 * Invalid because the configuration declares a value for 'value' and
* requires a value for the aliased 'locations'. So we likely end up with * requires a value for the aliased 'locations'. So we likely end up with
@@ -958,6 +1009,21 @@ public class AnnotatedElementUtilsTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@interface TestComponentScan { @interface TestComponentScan {
@AliasFor(attribute = "basePackages", annotation = ComponentScan.class)
String[] packages();
}
@ComponentScan
@Retention(RetentionPolicy.RUNTIME)
@interface ConventionBasedSinglePackageComponentScan {
String basePackages();
}
@ComponentScan
@Retention(RetentionPolicy.RUNTIME)
@interface AliasForBasedSinglePackageComponentScan {
@AliasFor(attribute = "basePackages", annotation = ComponentScan.class) @AliasFor(attribute = "basePackages", annotation = ComponentScan.class)
String pkg(); String pkg();
} }
@@ -1132,10 +1198,18 @@ public class AnnotatedElementUtilsTests {
static class TransitiveImplicitAliasesContextConfigClass { static class TransitiveImplicitAliasesContextConfigClass {
} }
@SingleLocationTransitiveImplicitAliasesContextConfig(groovy = "test.groovy")
static class SingleLocationTransitiveImplicitAliasesContextConfigClass {
}
@TransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml") @TransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml")
static class TransitiveImplicitAliasesWithSkippedLevelContextConfigClass { static class TransitiveImplicitAliasesWithSkippedLevelContextConfigClass {
} }
@SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml")
static class SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass {
}
@ComposedImplicitAliasesContextConfig @ComposedImplicitAliasesContextConfig
static class ComposedImplicitAliasesContextConfigClass { static class ComposedImplicitAliasesContextConfigClass {
} }
@@ -1152,10 +1226,18 @@ public class AnnotatedElementUtilsTests {
static class ComponentScanWithBasePackagesAndValueAliasClass { static class ComponentScanWithBasePackagesAndValueAliasClass {
} }
@TestComponentScan(pkg = "com.example.app.test") @TestComponentScan(packages = "com.example.app.test")
static class TestComponentScanClass { static class TestComponentScanClass {
} }
@ConventionBasedSinglePackageComponentScan(basePackages = "com.example.app.test")
static class ConventionBasedSinglePackageComponentScanClass {
}
@AliasForBasedSinglePackageComponentScan(pkg = "com.example.app.test")
static class AliasForBasedSinglePackageComponentScanClass {
}
@SpringAppConfig(Number.class) @SpringAppConfig(Number.class)
static class SpringAppConfigClass { static class SpringAppConfigClass {
} }

View File

@@ -465,8 +465,8 @@ public class AnnotationUtilsTests {
assertNotNull(attributes); assertNotNull(attributes);
assertEquals(WebMapping.class, attributes.annotationType()); assertEquals(WebMapping.class, attributes.annotationType());
assertEquals("name attribute: ", "foo", attributes.getString("name")); assertEquals("name attribute: ", "foo", attributes.getString("name"));
assertEquals("value attribute: ", "/test", attributes.getString(VALUE)); assertArrayEquals("value attribute: ", asArray("/test"), attributes.getStringArray(VALUE));
assertEquals("path attribute: ", "/test", attributes.getString("path")); assertArrayEquals("path attribute: ", asArray("/test"), attributes.getStringArray("path"));
method = WebController.class.getMethod("handleMappedWithPathAttribute"); method = WebController.class.getMethod("handleMappedWithPathAttribute");
webMapping = method.getAnnotation(WebMapping.class); webMapping = method.getAnnotation(WebMapping.class);
@@ -474,15 +474,18 @@ public class AnnotationUtilsTests {
assertNotNull(attributes); assertNotNull(attributes);
assertEquals(WebMapping.class, attributes.annotationType()); assertEquals(WebMapping.class, attributes.annotationType());
assertEquals("name attribute: ", "bar", attributes.getString("name")); assertEquals("name attribute: ", "bar", attributes.getString("name"));
assertEquals("value attribute: ", "/test", attributes.getString(VALUE)); assertArrayEquals("value attribute: ", asArray("/test"), attributes.getStringArray(VALUE));
assertEquals("path attribute: ", "/test", attributes.getString("path")); assertArrayEquals("path attribute: ", asArray("/test"), attributes.getStringArray("path"));
}
@Test
public void getAnnotationAttributesWithAttributeAliasesWithDifferentValues() throws Exception {
exception.expect(AnnotationConfigurationException.class); exception.expect(AnnotationConfigurationException.class);
exception.expectMessage(containsString("attribute 'value' and its alias 'path'")); exception.expectMessage(containsString("attribute 'value' and its alias 'path'"));
exception.expectMessage(containsString("values of [/enigma] and [/test]")); exception.expectMessage(containsString("values of [{/enigma}] and [{/test}]"));
method = WebController.class.getMethod("handleMappedWithDifferentPathAndValueAttributes"); Method method = WebController.class.getMethod("handleMappedWithDifferentPathAndValueAttributes");
webMapping = method.getAnnotation(WebMapping.class); WebMapping webMapping = method.getAnnotation(WebMapping.class);
getAnnotationAttributes(webMapping); getAnnotationAttributes(webMapping);
} }
@@ -835,8 +838,8 @@ public class AnnotationUtilsTests {
assertSame(synthesizedWebMapping, synthesizedAgainWebMapping); assertSame(synthesizedWebMapping, synthesizedAgainWebMapping);
assertEquals("name attribute: ", "foo", synthesizedAgainWebMapping.name()); assertEquals("name attribute: ", "foo", synthesizedAgainWebMapping.name());
assertEquals("aliased path attribute: ", "/test", synthesizedAgainWebMapping.path()); assertArrayEquals("aliased path attribute: ", asArray("/test"), synthesizedAgainWebMapping.path());
assertEquals("actual value attribute: ", "/test", synthesizedAgainWebMapping.value()); assertArrayEquals("actual value attribute: ", asArray("/test"), synthesizedAgainWebMapping.value());
} }
@Test @Test
@@ -956,16 +959,16 @@ public class AnnotationUtilsTests {
assertNotSame(webMapping, synthesizedWebMapping1); assertNotSame(webMapping, synthesizedWebMapping1);
assertEquals("name attribute: ", "foo", synthesizedWebMapping1.name()); assertEquals("name attribute: ", "foo", synthesizedWebMapping1.name());
assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping1.path()); assertArrayEquals("aliased path attribute: ", asArray("/test"), synthesizedWebMapping1.path());
assertEquals("actual value attribute: ", "/test", synthesizedWebMapping1.value()); assertArrayEquals("actual value attribute: ", asArray("/test"), synthesizedWebMapping1.value());
WebMapping synthesizedWebMapping2 = synthesizeAnnotation(webMapping); WebMapping synthesizedWebMapping2 = synthesizeAnnotation(webMapping);
assertThat(synthesizedWebMapping2, instanceOf(SynthesizedAnnotation.class)); assertThat(synthesizedWebMapping2, instanceOf(SynthesizedAnnotation.class));
assertNotSame(webMapping, synthesizedWebMapping2); assertNotSame(webMapping, synthesizedWebMapping2);
assertEquals("name attribute: ", "foo", synthesizedWebMapping2.name()); assertEquals("name attribute: ", "foo", synthesizedWebMapping2.name());
assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping2.path()); assertArrayEquals("aliased path attribute: ", asArray("/test"), synthesizedWebMapping2.path());
assertEquals("actual value attribute: ", "/test", synthesizedWebMapping2.value()); assertArrayEquals("actual value attribute: ", asArray("/test"), synthesizedWebMapping2.value());
} }
@Test @Test
@@ -1182,6 +1185,21 @@ public class AnnotationUtilsTests {
assertEquals("location: ", "test.xml", contextConfig.location()); assertEquals("location: ", "test.xml", contextConfig.location());
} }
@Test
public void synthesizeAnnotationFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() throws Exception {
Map<String, Object> map = Collections.singletonMap("value", "/foo");
Get get = synthesizeAnnotation(map, Get.class, null);
assertNotNull(get);
assertEquals("value: ", "/foo", get.value());
assertEquals("path: ", "/foo", get.path());
map = Collections.singletonMap("path", "/foo");
get = synthesizeAnnotation(map, Get.class, null);
assertNotNull(get);
assertEquals("value: ", "/foo", get.value());
assertEquals("path: ", "/foo", get.path());
}
@Test @Test
public void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception { public void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception {
assertAnnotationSynthesisFromMapWithImplicitAliases("value"); assertAnnotationSynthesisFromMapWithImplicitAliases("value");
@@ -1287,8 +1305,8 @@ public class AnnotationUtilsTests {
private void assertToStringForWebMappingWithPathAndValue(WebMapping webMapping) { private void assertToStringForWebMappingWithPathAndValue(WebMapping webMapping) {
String string = webMapping.toString(); String string = webMapping.toString();
assertThat(string, startsWith("@" + WebMapping.class.getName() + "(")); assertThat(string, startsWith("@" + WebMapping.class.getName() + "("));
assertThat(string, containsString("value=/test")); assertThat(string, containsString("value=[/test]"));
assertThat(string, containsString("path=/test")); assertThat(string, containsString("path=[/test]"));
assertThat(string, containsString("name=bar")); assertThat(string, containsString("name=bar"));
assertThat(string, containsString("method=")); assertThat(string, containsString("method="));
assertThat(string, containsString("[GET, POST]")); assertThat(string, containsString("[GET, POST]"));
@@ -1461,6 +1479,18 @@ public class AnnotationUtilsTests {
assertArrayEquals(new char[] { 'x', 'y', 'z' }, chars); assertArrayEquals(new char[] { 'x', 'y', 'z' }, chars);
} }
@SafeVarargs
// The following "varargs" suppression is necessary for javac from OpenJDK
// (1.8.0_60-b27); however, Eclipse warns that it's unnecessary. See the following
// Eclipse issues for details.
//
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=344783
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=349669#c10
// @SuppressWarnings("varargs")
static <T> T[] asArray(T... arr) {
return arr;
}
@Component("meta1") @Component("meta1")
@Order @Order
@@ -1768,14 +1798,40 @@ public class AnnotationUtilsTests {
String name(); String name();
@AliasFor("path") @AliasFor("path")
String value() default ""; String[] value() default "";
@AliasFor(attribute = "value") @AliasFor(attribute = "value")
String path() default ""; String[] path() default "";
RequestMethod[] method() default {}; RequestMethod[] method() default {};
} }
/**
* Mock of {@code org.springframework.web.bind.annotation.GetMapping}, except
* that the String arrays are overridden with single String elements.
*/
@Retention(RetentionPolicy.RUNTIME)
@WebMapping(method = RequestMethod.GET, name = "")
@interface Get {
@AliasFor(annotation = WebMapping.class)
String value() default "";
@AliasFor(annotation = WebMapping.class)
String path() default "";
}
/**
* Mock of {@code org.springframework.web.bind.annotation.PostMapping}, except
* that the path is overridden by convention with single String element.
*/
@Retention(RetentionPolicy.RUNTIME)
@WebMapping(method = RequestMethod.POST, name = "")
@interface Post {
String path() default "";
}
@Component("webController") @Component("webController")
static class WebController { static class WebController {
@@ -1787,6 +1843,18 @@ public class AnnotationUtilsTests {
public void handleMappedWithPathAttribute() { public void handleMappedWithPathAttribute() {
} }
@Get("/test")
public void getMappedWithValueAttribute() {
}
@Get(path = "/test")
public void getMappedWithPathAttribute() {
}
@Post(path = "/test")
public void postMappedWithPathAttribute() {
}
/** /**
* mapping is logically "equal" to handleMappedWithPathAttribute(). * mapping is logically "equal" to handleMappedWithPathAttribute().
*/ */

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2015 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -26,11 +26,14 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig; import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig;
import org.springframework.core.annotation.AnnotationUtilsTests.RequestMethod;
import org.springframework.core.annotation.AnnotationUtilsTests.WebMapping;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.core.annotation.AnnotationUtilsTests.*;
/** /**
* Unit tests for {@link MapAnnotationAttributeExtractor}. * Unit tests for {@link MapAnnotationAttributeExtractor}.
@@ -38,6 +41,7 @@ import static org.junit.Assert.*;
* @author Sam Brannen * @author Sam Brannen
* @since 4.2.1 * @since 4.2.1
*/ */
@SuppressWarnings("serial")
public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase { public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase {
@Before @Before
@@ -46,7 +50,6 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
} }
@Test @Test
@SuppressWarnings("serial")
public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() { public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() {
Map<String, Object> attributes = new HashMap<String, Object>(); Map<String, Object> attributes = new HashMap<String, Object>();
Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{ Map<String, Object> expectedAttributes = new HashMap<String, Object>() {{
@@ -64,7 +67,6 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
} }
@Test @Test
@SuppressWarnings("serial")
public void enrichAndValidateAttributesWithImplicitAliases() { public void enrichAndValidateAttributesWithImplicitAliases() {
Map<String, Object> attributes = new HashMap<String, Object>() {{ Map<String, Object> attributes = new HashMap<String, Object>() {{
put("groovyScript", "groovy!"); put("groovyScript", "groovy!");
@@ -84,6 +86,31 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
assertEnrichAndValidateAttributes(attributes, expectedAttributes); assertEnrichAndValidateAttributes(attributes, expectedAttributes);
} }
@Test
public void enrichAndValidateAttributesWithSingleElementThatOverridesAnArray() {
// @formatter:off
Map<String, Object> attributes = new HashMap<String, Object>() {{
// Intentionally storing 'value' as a single String instead of an array.
// put("value", asArray("/foo"));
put("value", "/foo");
put("name", "test");
}};
Map<String, Object> expected = new HashMap<String, Object>() {{
put("value", asArray("/foo"));
put("path", asArray("/foo"));
put("name", "test");
put("method", new RequestMethod[0]);
}};
// @formatter:on
MapAnnotationAttributeExtractor extractor = new MapAnnotationAttributeExtractor(attributes, WebMapping.class, null);
Map<String, Object> enriched = extractor.getSource();
assertEquals("attribute map size", expected.size(), enriched.size());
expected.forEach((attr, expectedValue) -> assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expectedValue)));
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void assertEnrichAndValidateAttributes(Map<String, Object> sourceAttributes, Map<String, Object> expected) { private void assertEnrichAndValidateAttributes(Map<String, Object> sourceAttributes, Map<String, Object> expected) {
Class<? extends Annotation> annotationType = ImplicitAliasesContextConfig.class; Class<? extends Annotation> annotationType = ImplicitAliasesContextConfig.class;
@@ -114,8 +141,7 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno
Map<String, Object> enriched = extractor.getSource(); Map<String, Object> enriched = extractor.getSource();
assertEquals("attribute map size", expected.size(), enriched.size()); assertEquals("attribute map size", expected.size(), enriched.size());
expected.keySet().stream().forEach( attr -> expected.forEach((attr, expectedValue) -> assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expectedValue)));
assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expected.get(attr))));
} }
@Override @Override