From eb654dc177eee8ed9d5e3f03f879d1fa9b1bf4ef Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 7 Mar 2016 13:27:27 +0100 Subject: [PATCH] Allow single element to override array in synthesized annotation This commit picks up where 8ff9e818a53aed90da80e68f4948147f07bec394 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 --- .../MapAnnotationAttributeExtractor.java | 11 +- .../AnnotatedElementUtilsTests.java | 184 +++++++++++++----- .../core/annotation/AnnotationUtilsTests.java | 102 ++++++++-- .../MapAnnotationAttributeExtractorTests.java | 36 +++- 4 files changed, 259 insertions(+), 74 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java index 7bb920f744..15ca984e81 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MapAnnotationAttributeExtractor.java @@ -18,6 +18,7 @@ package org.springframework.core.annotation; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.List; @@ -132,8 +133,16 @@ class MapAnnotationAttributeExtractor extends AbstractAliasAwareAnnotationAttrib if (!ClassUtils.isAssignable(requiredReturnType, actualReturnType)) { 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? - if (Annotation.class.isAssignableFrom(requiredReturnType) && + else if (Annotation.class.isAssignableFrom(requiredReturnType) && Map.class.isAssignableFrom(actualReturnType)) { Class nestedAnnotationType = (Class) requiredReturnType; diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index 680a7505b9..824f1ec5fa 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -33,8 +33,11 @@ import javax.annotation.Resource; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +import org.junit.internal.ArrayComparisonFailure; 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.util.Assert; import org.springframework.util.MultiValueMap; @@ -44,6 +47,7 @@ import static java.util.stream.Collectors.*; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.core.annotation.AnnotatedElementUtils.*; +import static org.springframework.core.annotation.AnnotationUtilsTests.*; /** * Unit tests for {@link AnnotatedElementUtils}. @@ -379,11 +383,21 @@ public class AnnotatedElementUtilsTests { assertGetMergedAnnotation(TransitiveImplicitAliasesContextConfigClass.class, "test.groovy"); } + @Test + public void getMergedAnnotationWithTransitiveImplicitAliasesWithSingleElementOverridingAnArrayViaAliasFor() { + assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesContextConfigClass.class, "test.groovy"); + } + @Test public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevel() { assertGetMergedAnnotation(TransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml"); } + @Test + public void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevelWithSingleElementOverridingAnArrayViaAliasFor() { + assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml"); + } + private void assertGetMergedAnnotation(Class element, String... expected) { String name = ContextConfig.class.getName(); ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class); @@ -560,6 +574,53 @@ public class AnnotatedElementUtilsTests { 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 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 annotationType) { + Assert.notNull(annotationType, "annotationType must not be null"); + return AnnotatedElementUtils.findMergedAnnotationAttributes(element, annotationType.getName(), false, false); + } + @Test public void findMergedAnnotationWithAttributeAliasesInTargetAnnotation() { Class element = AliasedTransactionalComponentClass.class; @@ -593,38 +654,6 @@ public class AnnotatedElementUtilsTests { 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 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 public void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { final String[] EMPTY = new String[0]; @@ -638,6 +667,24 @@ public class AnnotatedElementUtilsTests { 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 public void javaLangAnnotationTypeViaFindMergedAnnotation() throws Exception { 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)); } - private Set names(Class... classes) { 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[] asArray(T... arr) { - return arr; - } - - private static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, Class 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 {}; } + @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 @Retention(RetentionPolicy.RUNTIME) @interface TransitiveImplicitAliasesWithSkippedLevelContextConfig { @@ -892,6 +932,17 @@ public class AnnotatedElementUtilsTests { 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 * requires a value for the aliased 'locations'. So we likely end up with @@ -958,6 +1009,21 @@ public class AnnotatedElementUtilsTests { @Retention(RetentionPolicy.RUNTIME) @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) String pkg(); } @@ -1132,10 +1198,18 @@ public class AnnotatedElementUtilsTests { static class TransitiveImplicitAliasesContextConfigClass { } + @SingleLocationTransitiveImplicitAliasesContextConfig(groovy = "test.groovy") + static class SingleLocationTransitiveImplicitAliasesContextConfigClass { + } + @TransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml") static class TransitiveImplicitAliasesWithSkippedLevelContextConfigClass { } + @SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml") + static class SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass { + } + @ComposedImplicitAliasesContextConfig static class ComposedImplicitAliasesContextConfigClass { } @@ -1152,10 +1226,18 @@ public class AnnotatedElementUtilsTests { static class ComponentScanWithBasePackagesAndValueAliasClass { } - @TestComponentScan(pkg = "com.example.app.test") + @TestComponentScan(packages = "com.example.app.test") static class TestComponentScanClass { } + @ConventionBasedSinglePackageComponentScan(basePackages = "com.example.app.test") + static class ConventionBasedSinglePackageComponentScanClass { + } + + @AliasForBasedSinglePackageComponentScan(pkg = "com.example.app.test") + static class AliasForBasedSinglePackageComponentScanClass { + } + @SpringAppConfig(Number.class) static class SpringAppConfigClass { } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index ae35941c5f..bf625f085e 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -465,8 +465,8 @@ public class AnnotationUtilsTests { assertNotNull(attributes); assertEquals(WebMapping.class, attributes.annotationType()); assertEquals("name attribute: ", "foo", attributes.getString("name")); - assertEquals("value attribute: ", "/test", attributes.getString(VALUE)); - assertEquals("path attribute: ", "/test", attributes.getString("path")); + assertArrayEquals("value attribute: ", asArray("/test"), attributes.getStringArray(VALUE)); + assertArrayEquals("path attribute: ", asArray("/test"), attributes.getStringArray("path")); method = WebController.class.getMethod("handleMappedWithPathAttribute"); webMapping = method.getAnnotation(WebMapping.class); @@ -474,15 +474,18 @@ public class AnnotationUtilsTests { assertNotNull(attributes); assertEquals(WebMapping.class, attributes.annotationType()); assertEquals("name attribute: ", "bar", attributes.getString("name")); - assertEquals("value attribute: ", "/test", attributes.getString(VALUE)); - assertEquals("path attribute: ", "/test", attributes.getString("path")); + assertArrayEquals("value attribute: ", asArray("/test"), attributes.getStringArray(VALUE)); + assertArrayEquals("path attribute: ", asArray("/test"), attributes.getStringArray("path")); + } + @Test + public void getAnnotationAttributesWithAttributeAliasesWithDifferentValues() throws Exception { exception.expect(AnnotationConfigurationException.class); 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"); - webMapping = method.getAnnotation(WebMapping.class); + Method method = WebController.class.getMethod("handleMappedWithDifferentPathAndValueAttributes"); + WebMapping webMapping = method.getAnnotation(WebMapping.class); getAnnotationAttributes(webMapping); } @@ -835,8 +838,8 @@ public class AnnotationUtilsTests { assertSame(synthesizedWebMapping, synthesizedAgainWebMapping); assertEquals("name attribute: ", "foo", synthesizedAgainWebMapping.name()); - assertEquals("aliased path attribute: ", "/test", synthesizedAgainWebMapping.path()); - assertEquals("actual value attribute: ", "/test", synthesizedAgainWebMapping.value()); + assertArrayEquals("aliased path attribute: ", asArray("/test"), synthesizedAgainWebMapping.path()); + assertArrayEquals("actual value attribute: ", asArray("/test"), synthesizedAgainWebMapping.value()); } @Test @@ -956,16 +959,16 @@ public class AnnotationUtilsTests { assertNotSame(webMapping, synthesizedWebMapping1); assertEquals("name attribute: ", "foo", synthesizedWebMapping1.name()); - assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping1.path()); - assertEquals("actual value attribute: ", "/test", synthesizedWebMapping1.value()); + assertArrayEquals("aliased path attribute: ", asArray("/test"), synthesizedWebMapping1.path()); + assertArrayEquals("actual value attribute: ", asArray("/test"), synthesizedWebMapping1.value()); WebMapping synthesizedWebMapping2 = synthesizeAnnotation(webMapping); assertThat(synthesizedWebMapping2, instanceOf(SynthesizedAnnotation.class)); assertNotSame(webMapping, synthesizedWebMapping2); assertEquals("name attribute: ", "foo", synthesizedWebMapping2.name()); - assertEquals("aliased path attribute: ", "/test", synthesizedWebMapping2.path()); - assertEquals("actual value attribute: ", "/test", synthesizedWebMapping2.value()); + assertArrayEquals("aliased path attribute: ", asArray("/test"), synthesizedWebMapping2.path()); + assertArrayEquals("actual value attribute: ", asArray("/test"), synthesizedWebMapping2.value()); } @Test @@ -1182,6 +1185,21 @@ public class AnnotationUtilsTests { assertEquals("location: ", "test.xml", contextConfig.location()); } + @Test + public void synthesizeAnnotationFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() throws Exception { + Map 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 public void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception { assertAnnotationSynthesisFromMapWithImplicitAliases("value"); @@ -1287,8 +1305,8 @@ public class AnnotationUtilsTests { private void assertToStringForWebMappingWithPathAndValue(WebMapping webMapping) { String string = webMapping.toString(); assertThat(string, startsWith("@" + WebMapping.class.getName() + "(")); - assertThat(string, containsString("value=/test")); - assertThat(string, containsString("path=/test")); + assertThat(string, containsString("value=[/test]")); + assertThat(string, containsString("path=[/test]")); assertThat(string, containsString("name=bar")); assertThat(string, containsString("method=")); assertThat(string, containsString("[GET, POST]")); @@ -1461,6 +1479,18 @@ public class AnnotationUtilsTests { 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[] asArray(T... arr) { + return arr; + } + @Component("meta1") @Order @@ -1768,14 +1798,40 @@ public class AnnotationUtilsTests { String name(); @AliasFor("path") - String value() default ""; + String[] value() default ""; @AliasFor(attribute = "value") - String path() default ""; + String[] path() 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") static class WebController { @@ -1787,6 +1843,18 @@ public class AnnotationUtilsTests { 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(). */ diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java index a38115272e..d3b456023b 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MapAnnotationAttributeExtractorTests.java @@ -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"); * 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.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.MultiValueMap; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.springframework.core.annotation.AnnotationUtilsTests.*; /** * Unit tests for {@link MapAnnotationAttributeExtractor}. @@ -38,6 +41,7 @@ import static org.junit.Assert.*; * @author Sam Brannen * @since 4.2.1 */ +@SuppressWarnings("serial") public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnnotationAttributeExtractorTestCase { @Before @@ -46,7 +50,6 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno } @Test - @SuppressWarnings("serial") public void enrichAndValidateAttributesWithImplicitAliasesAndMinimalAttributes() { Map attributes = new HashMap(); Map expectedAttributes = new HashMap() {{ @@ -64,7 +67,6 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno } @Test - @SuppressWarnings("serial") public void enrichAndValidateAttributesWithImplicitAliases() { Map attributes = new HashMap() {{ put("groovyScript", "groovy!"); @@ -84,6 +86,31 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno assertEnrichAndValidateAttributes(attributes, expectedAttributes); } + @Test + public void enrichAndValidateAttributesWithSingleElementThatOverridesAnArray() { + // @formatter:off + Map attributes = new HashMap() {{ + // Intentionally storing 'value' as a single String instead of an array. + // put("value", asArray("/foo")); + put("value", "/foo"); + put("name", "test"); + }}; + + Map expected = new HashMap() {{ + 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 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") private void assertEnrichAndValidateAttributes(Map sourceAttributes, Map expected) { Class annotationType = ImplicitAliasesContextConfig.class; @@ -114,8 +141,7 @@ public class MapAnnotationAttributeExtractorTests extends AbstractAliasAwareAnno Map 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)))); + expected.forEach((attr, expectedValue) -> assertThat("for attribute '" + attr + "'", enriched.get(attr), is(expectedValue))); } @Override