From edf4c833c2e7861750516e7c72980f599f0a4e5b Mon Sep 17 00:00:00 2001 From: Stefan Zwanenburg Date: Mon, 31 Aug 2020 12:21:20 +0200 Subject: [PATCH] Support nested @PropertyMapping annotations Update `AnnotationsPropertySource` so that nested annotations are supported. Prior to this commit, annotations annotated with `@PropertyMapping` that contained nested annotation attributes would result in instances of `TypeMappedAnnotation` being used as properties. This usually led to errors due to not being able to convert those to Strings. This commit makes it so that nested annotations are recursively mapped to properties. This should allow for more complex configuration to be mapped from annotations. See gh-23146 --- .../properties/AnnotationsPropertySource.java | 12 +++- .../AnnotationsPropertySourceTests.java | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java index 9e7d4dfee4..60c992294e 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java @@ -90,7 +90,7 @@ public class AnnotationsPropertySource extends EnumerablePropertySource } } String name = getName(prefix, attributeMapping, attribute); - putProperties(name, value.get(), properties); + putProperties(name, skip, value.get(), properties); } private String getName(String prefix, MergedAnnotation attributeMapping, Method attribute) { @@ -118,11 +118,17 @@ public class AnnotationsPropertySource extends EnumerablePropertySource return postfix; } - private void putProperties(String name, Object value, Map properties) { + private void putProperties(String name, SkipPropertyMapping defaultSkip, Object value, + Map properties) { if (ObjectUtils.isArray(value)) { Object[] array = ObjectUtils.toObjectArray(value); for (int i = 0; i < array.length; i++) { - properties.put(name + "[" + i + "]", array[i]); + putProperties(name + "[" + i + "]", defaultSkip, array[i], properties); + } + } + else if (value instanceof MergedAnnotation) { + for (Method attribute : ((MergedAnnotation) value).getType().getDeclaredMethods()) { + collectProperties(name, defaultSkip, (MergedAnnotation) value, attribute, properties); } } else { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java index 2744df4d8b..27edd3f723 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java @@ -21,6 +21,9 @@ import java.lang.annotation.RetentionPolicy; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.properties.AnnotationsPropertySourceTests.DeeplyNestedAnnotations.Level1; +import org.springframework.boot.test.autoconfigure.properties.AnnotationsPropertySourceTests.DeeplyNestedAnnotations.Level2; +import org.springframework.boot.test.autoconfigure.properties.AnnotationsPropertySourceTests.NestedAnnotations.Entry; import org.springframework.core.annotation.AliasFor; import static org.assertj.core.api.Assertions.assertThat; @@ -172,6 +175,26 @@ class AnnotationsPropertySourceTests { assertThat(source.containsProperty("testenum.value")).isFalse(); } + @Test + void nestedAnnotationsMapped() { + AnnotationsPropertySource source = new AnnotationsPropertySource(PropertyMappedWithNestedAnnotations.class); + assertThat(source.getProperty("testnested")).isNull(); + assertThat(source.getProperty("testnested.entries[0]")).isNull(); + assertThat(source.getProperty("testnested.entries[0].value")).isEqualTo("one"); + assertThat(source.getProperty("testnested.entries[1]")).isNull(); + assertThat(source.getProperty("testnested.entries[1].value")).isEqualTo("two"); + } + + @Test + void deeplyNestedAnnotationsMapped() { + AnnotationsPropertySource source = new AnnotationsPropertySource( + PropertyMappedWithDeeplyNestedAnnotations.class); + assertThat(source.getProperty("testdeeplynested")).isNull(); + assertThat(source.getProperty("testdeeplynested.level1")).isNull(); + assertThat(source.getProperty("testdeeplynested.level1.level2")).isNull(); + assertThat(source.getProperty("testdeeplynested.level1.level2.value")).isEqualTo("level2"); + } + static class NoAnnotation { } @@ -396,4 +419,51 @@ class AnnotationsPropertySourceTests { } + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping("testnested") + @interface NestedAnnotations { + + Entry[] entries(); + + @Retention(RetentionPolicy.RUNTIME) + @interface Entry { + + String value(); + + } + + } + + @NestedAnnotations(entries = { @Entry("one"), @Entry("two") }) + static class PropertyMappedWithNestedAnnotations { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping("testdeeplynested") + @interface DeeplyNestedAnnotations { + + Level1 level1(); + + @Retention(RetentionPolicy.RUNTIME) + @interface Level1 { + + Level2 level2(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Level2 { + + String value(); + + } + + } + + @DeeplyNestedAnnotations(level1 = @Level1(level2 = @Level2("level2"))) + static class PropertyMappedWithDeeplyNestedAnnotations { + + } + }