diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 1f190750228..92e0cbbb125 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * 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. @@ -292,7 +292,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor boolean isNested = isNested(returnTypeElement, field, element); boolean isCollection = this.typeUtils.isCollectionOrMap(returnType); if (!isExcluded && !isNested && (setter != null || isCollection)) { - String dataType = this.typeUtils.getType(returnType); + String dataType = this.typeUtils.getType(element, returnType); String sourceType = this.typeUtils.getQualifiedName(element); String description = this.typeUtils.getJavaDoc(field); Object defaultValue = fieldValues.get(name); @@ -335,7 +335,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor boolean isCollection = this.typeUtils.isCollectionOrMap(returnType); boolean hasSetter = hasLombokSetter(field, element); if (!isExcluded && !isNested && (hasSetter || isCollection)) { - String dataType = this.typeUtils.getType(returnType); + String dataType = this.typeUtils.getType(element, returnType); String sourceType = this.typeUtils.getQualifiedName(element); String description = this.typeUtils.getJavaDoc(field); Object defaultValue = fieldValues.get(name); 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 0b248964b91..d71fdbfc156 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * 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. @@ -21,7 +21,9 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -33,8 +35,10 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; import javax.lang.model.util.SimpleTypeVisitor8; import javax.lang.model.util.Types; +import javax.tools.Diagnostic.Kind; /** * Type Utilities. @@ -73,18 +77,22 @@ class TypeUtils { private final ProcessingEnvironment env; + private final Types types; + private final TypeExtractor typeExtractor; private final TypeMirror collectionType; private final TypeMirror mapType; + private final Map typeDescriptors = new HashMap<>(); + TypeUtils(ProcessingEnvironment env) { this.env = env; - Types types = env.getTypeUtils(); - this.typeExtractor = new TypeExtractor(types); - this.collectionType = getDeclaredType(types, Collection.class, 1); - this.mapType = getDeclaredType(types, Map.class, 2); + this.types = env.getTypeUtils(); + this.typeExtractor = new TypeExtractor(this.types); + this.collectionType = getDeclaredType(this.types, Collection.class, 1); + this.mapType = getDeclaredType(this.types, Map.class, 2); } private TypeMirror getDeclaredType(Types types, Class typeClass, @@ -115,14 +123,15 @@ class TypeUtils { /** * Return the type of the specified {@link TypeMirror} including all its generic * information. + * @param element the {@link TypeElement} in which this {@code type} is declared * @param type the type to handle * @return a representation of the type including all its generic information */ - public String getType(TypeMirror type) { + public String getType(TypeElement element, TypeMirror type) { if (type == null) { return null; } - return type.accept(this.typeExtractor, null); + return type.accept(this.typeExtractor, createTypeDescriptor(element)); } public boolean isCollectionOrMap(TypeMirror type) { @@ -160,11 +169,49 @@ class TypeUtils { return WRAPPER_TO_PRIMITIVE.get(type.toString()); } + TypeDescriptor resolveTypeDescriptor(TypeElement element) { + if (this.typeDescriptors.containsKey(element)) { + return this.typeDescriptors.get(element); + } + return createTypeDescriptor(element); + } + + private TypeDescriptor createTypeDescriptor(TypeElement element) { + TypeDescriptor descriptor = new TypeDescriptor(); + process(descriptor, element.asType()); + this.typeDescriptors.put(element, descriptor); + return descriptor; + } + + private void process(TypeDescriptor descriptor, TypeMirror type) { + try { + if (type.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) type; + DeclaredType freshType = (DeclaredType) this.env.getElementUtils() + .getTypeElement(this.types.asElement(type).toString()).asType(); + List arguments = declaredType.getTypeArguments(); + for (int i = 0; i < arguments.size(); i++) { + TypeMirror specificType = arguments.get(i); + TypeMirror signatureType = freshType.getTypeArguments().get(i); + descriptor.registerIfNecessary(signatureType, specificType); + } + TypeElement element = (TypeElement) this.types.asElement(type); + process(descriptor, element.getSuperclass()); + } + } + catch (Exception ex) { + this.env.getMessager().printMessage(Kind.WARNING, + "Failed to generated type descriptor for " + type, + this.types.asElement(type)); + } + } + /** * A visitor that extracts the fully qualified name of a type, including generic * information. */ - private static class TypeExtractor extends SimpleTypeVisitor8 { + private static class TypeExtractor + extends SimpleTypeVisitor8 { private final Types types; @@ -173,34 +220,60 @@ class TypeUtils { } @Override - public String visitDeclared(DeclaredType type, Void none) { + public String visitDeclared(DeclaredType type, TypeDescriptor descriptor) { TypeElement enclosingElement = getEnclosingTypeElement(type); - if (enclosingElement != null) { - return getQualifiedName(enclosingElement) + "$" - + type.asElement().getSimpleName(); - } - String qualifiedName = getQualifiedName(type.asElement()); + String qualifiedName = determineQualifiedName(type, enclosingElement); if (type.getTypeArguments().isEmpty()) { return qualifiedName; } StringBuilder name = new StringBuilder(); name.append(qualifiedName); name.append("<").append(type.getTypeArguments().stream() - .map(TypeMirror::toString).collect(Collectors.joining(","))) + .map((t) -> visit(t, descriptor)).collect(Collectors.joining(","))) .append(">"); return name.toString(); } - @Override - public String visitArray(ArrayType t, Void none) { - return t.getComponentType().accept(this, none) + "[]"; + private String determineQualifiedName(DeclaredType type, + TypeElement enclosingElement) { + if (enclosingElement != null) { + return getQualifiedName(enclosingElement) + "$" + + type.asElement().getSimpleName(); + } + return getQualifiedName(type.asElement()); } @Override - public String visitPrimitive(PrimitiveType t, Void none) { + public String visitTypeVariable(TypeVariable t, TypeDescriptor descriptor) { + 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); + } + else { + return visit(typeMirror, descriptor); + } + } + // Unresolved generics, use upper bound + return visit(t.getUpperBound(), descriptor); + } + + @Override + public String visitArray(ArrayType t, TypeDescriptor descriptor) { + return t.getComponentType().accept(this, descriptor) + "[]"; + } + + @Override + public String visitPrimitive(PrimitiveType t, TypeDescriptor descriptor) { return this.types.boxedClass(t).getQualifiedName().toString(); } + @Override + protected String defaultAction(TypeMirror t, TypeDescriptor descriptor) { + return t.toString(); + } + public String getQualifiedName(Element element) { if (element == null) { return null; @@ -230,4 +303,42 @@ class TypeUtils { } + /** + * Descriptor for a given type. + */ + static class TypeDescriptor { + + private final Map generics = new HashMap<>(); + + public Map getGenerics() { + return Collections.unmodifiableMap(this.generics); + } + + public TypeMirror resolveGeneric(TypeVariable typeVariable) { + return resolveGeneric(getParameterName(typeVariable)); + } + + public TypeMirror resolveGeneric(String parameterName) { + return this.generics.entrySet().stream() + .filter((e) -> getParameterName(e.getKey()).equals(parameterName)) + .findFirst().map(Entry::getValue).orElse(null); + } + + private void registerIfNecessary(TypeMirror variable, TypeMirror resolution) { + if (variable instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable) variable; + if (this.generics.keySet().stream() + .noneMatch((candidate) -> getParameterName(candidate) + .equals(getParameterName(typeVariable)))) { + this.generics.put(typeVariable, resolution); + } + } + } + + private String getParameterName(TypeVariable typeVariable) { + return typeVariable.asElement().getSimpleName().toString(); + } + + } + } 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 001e11a8d19..efb4f8be26e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * 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. @@ -45,6 +45,9 @@ import org.springframework.boot.configurationsample.endpoint.EnabledEndpoint; 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.SimpleGenericProperties; +import org.springframework.boot.configurationsample.generic.UnresolvedGenericProperties; import org.springframework.boot.configurationsample.incremental.BarProperties; import org.springframework.boot.configurationsample.incremental.FooProperties; import org.springframework.boot.configurationsample.incremental.RenamedBarProperties; @@ -292,7 +295,7 @@ public class ConfigurationMetadataAnnotationProcessorTests { assertThat(metadata).has(Metadata.withProperty("collection.doubles", "java.util.List")); assertThat(metadata).has(Metadata.withProperty("collection.names-to-holders", - "java.util.Map>")); + "java.util.Map>")); } @Test @@ -504,6 +507,40 @@ public class ConfigurationMetadataAnnotationProcessorTests { .withMessageContaining("Compilation failed"); } + @Test + public void simpleGenericProperties() { + ConfigurationMetadata metadata = compile(AbstractGenericProperties.class, + SimpleGenericProperties.class); + assertThat(metadata).has( + Metadata.withGroup("generic").fromSource(SimpleGenericProperties.class)); + assertThat(metadata).has(Metadata.withProperty("generic.name", String.class) + .fromSource(SimpleGenericProperties.class) + .withDescription("Generic name.").withDefaultValue(null)); + assertThat(metadata).has(Metadata + .withProperty("generic.mappings", + "java.util.Map") + .fromSource(SimpleGenericProperties.class) + .withDescription("Generic mappings.").withDefaultValue(null)); + assertThat(metadata.getItems()).hasSize(3); + } + + @Test + public void unresolvedGenericProperties() { + ConfigurationMetadata metadata = compile(AbstractGenericProperties.class, + UnresolvedGenericProperties.class); + assertThat(metadata).has(Metadata.withGroup("generic") + .fromSource(UnresolvedGenericProperties.class)); + assertThat(metadata).has(Metadata.withProperty("generic.name", String.class) + .fromSource(UnresolvedGenericProperties.class) + .withDescription("Generic name.").withDefaultValue(null)); + assertThat(metadata).has(Metadata + .withProperty("generic.mappings", + "java.util.Map") + .fromSource(UnresolvedGenericProperties.class) + .withDescription("Generic mappings.").withDefaultValue(null)); + assertThat(metadata.getItems()).hasSize(3); + } + @Test public void genericTypes() { ConfigurationMetadata metadata = compile(GenericConfig.class); @@ -518,7 +555,7 @@ public class ConfigurationMetadataAnnotationProcessorTests { assertThat(metadata).has(Metadata.withProperty("generic.foo.name") .ofType(String.class).fromSource(GenericConfig.Foo.class)); assertThat(metadata).has(Metadata.withProperty("generic.foo.string-to-bar") - .ofType("java.util.Map>") + .ofType("java.util.Map>") .fromSource(GenericConfig.Foo.class)); assertThat(metadata).has(Metadata.withProperty("generic.foo.string-to-integer") .ofType("java.util.Map") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TypeUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TypeUtilsTests.java new file mode 100644 index 00000000000..564c644ae79 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TypeUtilsTests.java @@ -0,0 +1,136 @@ +/* + * 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 + * + * 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.boot.configurationprocessor; + +import java.io.IOException; +import java.time.Duration; +import java.util.Set; +import java.util.function.BiConsumer; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.springframework.boot.configurationprocessor.TypeUtils.TypeDescriptor; +import org.springframework.boot.configurationsample.generic.AbstractGenericProperties; +import org.springframework.boot.configurationsample.generic.AbstractIntermediateGenericProperties; +import org.springframework.boot.configurationsample.generic.SimpleGenericProperties; +import org.springframework.boot.testsupport.compiler.TestCompiler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TypeUtils}. + * + * @author Stephane Nicoll + */ +public class TypeUtilsTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void resolveTypeDescriptorOnConcreteClass() throws IOException { + process(SimpleGenericProperties.class, (roundEnv, typeUtils) -> { + for (Element rootElement : roundEnv.getRootElements()) { + TypeDescriptor typeDescriptor = typeUtils + .resolveTypeDescriptor((TypeElement) rootElement); + assertThat(typeDescriptor.getGenerics().keySet().stream() + .map(Object::toString)).containsOnly("A", "B", "C"); + assertThat(typeDescriptor.resolveGeneric("A")) + .hasToString(String.class.getName()); + assertThat(typeDescriptor.resolveGeneric("B")) + .hasToString(Integer.class.getName()); + assertThat(typeDescriptor.resolveGeneric("C")) + .hasToString(Duration.class.getName()); + } + }); + } + + @Test + public void resolveTypeDescriptorOnIntermediateClass() throws IOException { + process(AbstractIntermediateGenericProperties.class, (roundEnv, typeUtils) -> { + for (Element rootElement : roundEnv.getRootElements()) { + TypeDescriptor typeDescriptor = typeUtils + .resolveTypeDescriptor((TypeElement) rootElement); + assertThat(typeDescriptor.getGenerics().keySet().stream() + .map(Object::toString)).containsOnly("A", "B", "C"); + assertThat(typeDescriptor.resolveGeneric("A")) + .hasToString(String.class.getName()); + assertThat(typeDescriptor.resolveGeneric("B")) + .hasToString(Integer.class.getName()); + assertThat(typeDescriptor.resolveGeneric("C")).hasToString("C"); + } + }); + } + + @Test + public void resolveTypeDescriptorWithOnlyGenerics() throws IOException { + process(AbstractGenericProperties.class, (roundEnv, typeUtils) -> { + for (Element rootElement : roundEnv.getRootElements()) { + TypeDescriptor typeDescriptor = typeUtils + .resolveTypeDescriptor((TypeElement) rootElement); + assertThat(typeDescriptor.getGenerics().keySet().stream() + .map(Object::toString)).containsOnly("A", "B", "C"); + + } + }); + } + + private void process(Class target, + BiConsumer consumer) throws IOException { + TestProcessor processor = new TestProcessor(consumer); + TestCompiler compiler = new TestCompiler(this.temporaryFolder); + compiler.getTask(target).call(processor); + } + + @SupportedAnnotationTypes("*") + @SupportedSourceVersion(SourceVersion.RELEASE_8) + private final class TestProcessor extends AbstractProcessor { + + private final BiConsumer typeUtilsConsumer; + + private TypeUtils typeUtils; + + private TestProcessor(BiConsumer typeUtils) { + this.typeUtilsConsumer = typeUtils; + } + + @Override + public synchronized void init(ProcessingEnvironment env) { + this.typeUtils = new TypeUtils(env); + } + + @Override + public boolean process(Set annotations, + RoundEnvironment roundEnv) { + this.typeUtilsConsumer.accept(roundEnv, this.typeUtils); + return false; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/AbstractGenericProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/AbstractGenericProperties.java new file mode 100644 index 00000000000..844ba0f6c67 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/AbstractGenericProperties.java @@ -0,0 +1,51 @@ +/* + * 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 + * + * 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.boot.configurationsample.generic; + +import java.util.HashMap; +import java.util.Map; + +/** + * A base properties class with generics. + * + * @author Stephane Nicoll + */ +public class AbstractGenericProperties { + + /** + * Generic name. + */ + private A name; + + /** + * Generic mappings. + */ + private final Map mappings = new HashMap<>(); + + public A getName() { + return this.name; + } + + public void setName(A name) { + this.name = name; + } + + public Map getMappings() { + return this.mappings; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/AbstractIntermediateGenericProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/AbstractIntermediateGenericProperties.java new file mode 100644 index 00000000000..032eb01cc73 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/AbstractIntermediateGenericProperties.java @@ -0,0 +1,27 @@ +/* + * 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 + * + * 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.boot.configurationsample.generic; + +/** + * An intermediate layer that resolves some of the generics from the parent but not all. + * + * @author Stephane Nicoll + */ +public abstract class AbstractIntermediateGenericProperties + extends AbstractGenericProperties { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/SimpleGenericProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/SimpleGenericProperties.java new file mode 100644 index 00000000000..b770e8bf8b9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/SimpleGenericProperties.java @@ -0,0 +1,32 @@ +/* + * 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 + * + * 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.boot.configurationsample.generic; + +import java.time.Duration; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Simple properties with resolved generic information. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("generic") +public class SimpleGenericProperties + extends AbstractIntermediateGenericProperties { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/UnresolvedGenericProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/UnresolvedGenericProperties.java new file mode 100644 index 00000000000..b341feb4117 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/UnresolvedGenericProperties.java @@ -0,0 +1,30 @@ +/* + * 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 + * + * 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.boot.configurationsample.generic; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Properties with unresolved generic information. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("generic") +public class UnresolvedGenericProperties + extends AbstractGenericProperties { + +}