diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java index a648546927..d6f8ca63bd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java @@ -248,15 +248,28 @@ class TypeUtils { TypeMirror typeMirror = descriptor.resolveGeneric(t); if (typeMirror != null) { if (typeMirror instanceof TypeVariable) { - // Still unresolved, let's use upper bound - return visit(((TypeVariable) typeMirror).getUpperBound(), descriptor); + TypeVariable typeVariable = (TypeVariable) typeMirror; + // Still unresolved, let's use the upper bound, checking first if + // a cycle may exist + if (!hasCycle(typeVariable)) { + return visit(typeVariable.getUpperBound(), descriptor); + } } else { return visit(typeMirror, descriptor); } } - // Unresolved generics, use upper bound - return visit(t.getUpperBound(), descriptor); + // Fallback to simple representation of the upper bound + return defaultAction(t.getUpperBound(), descriptor); + } + + private boolean hasCycle(TypeVariable variable) { + TypeMirror upperBound = variable.getUpperBound(); + if (upperBound instanceof DeclaredType) { + return ((DeclaredType) upperBound).getTypeArguments().stream() + .anyMatch((candidate) -> candidate.equals(variable)); + } + return false; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index f847e2e5ed..7da1048aa0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -46,8 +46,10 @@ import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint; import org.springframework.boot.configurationsample.endpoint.SpecificEndpoint; import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalEndpoint; import org.springframework.boot.configurationsample.generic.AbstractGenericProperties; +import org.springframework.boot.configurationsample.generic.ComplexGenericProperties; import org.springframework.boot.configurationsample.generic.SimpleGenericProperties; import org.springframework.boot.configurationsample.generic.UnresolvedGenericProperties; +import org.springframework.boot.configurationsample.generic.UpperBoundGenericPojo; import org.springframework.boot.configurationsample.incremental.BarProperties; import org.springframework.boot.configurationsample.incremental.FooProperties; import org.springframework.boot.configurationsample.incremental.RenamedBarProperties; @@ -524,6 +526,21 @@ public class ConfigurationMetadataAnnotationProcessorTests { assertThat(metadata.getItems()).hasSize(3); } + @Test + public void complexGenericProperties() { + ConfigurationMetadata metadata = compile(ComplexGenericProperties.class); + assertThat(metadata).has( + Metadata.withGroup("generic").fromSource(ComplexGenericProperties.class)); + assertThat(metadata).has( + Metadata.withGroup("generic.test").ofType(UpperBoundGenericPojo.class) + .fromSource(ComplexGenericProperties.class)); + assertThat(metadata).has(Metadata + .withProperty("generic.test.mappings", + "java.util.Map,java.lang.String>") + .fromSource(UpperBoundGenericPojo.class)); + assertThat(metadata.getItems()).hasSize(3); + } + @Test public void unresolvedGenericProperties() { ConfigurationMetadata metadata = compile(AbstractGenericProperties.class, diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ComplexGenericProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ComplexGenericProperties.java new file mode 100644 index 0000000000..3e1d358c98 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ComplexGenericProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.boot.configurationsample.generic; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.NestedConfigurationProperty; + +/** + * More advanced generic setup. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("generic") +public class ComplexGenericProperties { + + @NestedConfigurationProperty + private UpperBoundGenericPojo test = new UpperBoundGenericPojo<>(); + + public UpperBoundGenericPojo getTest() { + return this.test; + } + + public enum Test { + + ONE, TWO, THREE + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/UpperBoundGenericPojo.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/UpperBoundGenericPojo.java new file mode 100644 index 0000000000..da903d38d6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/UpperBoundGenericPojo.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.boot.configurationsample.generic; + +import java.util.HashMap; +import java.util.Map; + +/** + * A pojo with a complex generic signature. + * + * @author Stephane Nicoll + */ +public class UpperBoundGenericPojo> { + + private final Map mappings = new HashMap<>(); + + public Map getMappings() { + return this.mappings; + } + +}