diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix/configuration-metadata.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix/configuration-metadata.adoc index b07144d773..6d953c37e9 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix/configuration-metadata.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix/configuration-metadata.adoc @@ -809,8 +809,11 @@ is used to populate the `description` attribute. NOTE: You should only use simple text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON. -Properties are discovered through the presence of standard getters and setters with -special handling for collection types (that is detected even if only a getter is present). +If the class has a single constructor with at least one parameters, one property is +created per constructor parameter. Otherwise, properties are discovered through the +presence of standard getters and setters with special handling for collection types (that +is detected even if only a getter is present). + The annotation processor also supports the use of the `@Data`, `@Getter`, and `@Setter` lombok annotations. 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 ce11fd1cd3..f1d20de69d 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 @@ -73,6 +73,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot." + "context.properties.DeprecatedConfigurationProperty"; + static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot." + + "context.properties.bind.DefaultValue"; + static final String ENDPOINT_ANNOTATION = "org.springframework.boot.actuate." + "endpoint.annotation.Endpoint"; @@ -100,6 +103,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor return DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION; } + protected String defaultValueAnnotation() { + return DEFAULT_VALUE_ANNOTATION; + } + protected String endpointAnnotation() { return ENDPOINT_ANNOTATION; } @@ -127,8 +134,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(), nestedConfigurationPropertyAnnotation(), - deprecatedConfigurationPropertyAnnotation(), endpointAnnotation(), - readOperationAnnotation()); + deprecatedConfigurationPropertyAnnotation(), defaultValueAnnotation(), + endpointAnnotation(), readOperationAnnotation()); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java new file mode 100644 index 0000000000..e83f3930b9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java @@ -0,0 +1,224 @@ +/* + * 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.configurationprocessor; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.TypeKindVisitor8; +import javax.tools.Diagnostic.Kind; + +/** + * A {@link PropertyDescriptor} for a constructor parameter. + * + * @author Stephane Nicoll + */ +class ConstructorParameterPropertyDescriptor extends PropertyDescriptor { + + ConstructorParameterPropertyDescriptor(TypeElement ownerElement, + ExecutableElement factoryMethod, VariableElement source, String name, + TypeMirror type, VariableElement field, ExecutableElement getter, + ExecutableElement setter) { + super(ownerElement, factoryMethod, source, name, type, field, getter, setter); + } + + @Override + protected boolean isProperty(MetadataGenerationEnvironment env) { + // If it's a constructor parameter, it doesn't matter as we must be able to bind + // it to build the object. + return !isNested(env); + } + + @Override + protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) { + Object defaultValue = getDefaultValueFromAnnotation(environment, getSource()); + if (defaultValue != null) { + return defaultValue; + } + return getSource().asType().accept(DefaultPrimitiveTypeVisitor.INSTANCE, null); + } + + private Object getDefaultValueFromAnnotation( + MetadataGenerationEnvironment environment, Element element) { + AnnotationMirror defaultValueAnnotation = environment + .getDefaultValueAnnotation(element); + if (defaultValueAnnotation != null) { + List defaultValue = (List) environment + .getAnnotationElementValues(defaultValueAnnotation).get("value"); + if (defaultValue != null) { + try { + TypeMirror specificType = determineSpecificType(environment); + if (defaultValue.size() == 1) { + return coerceValue(specificType, defaultValue.get(0)); + } + return defaultValue.stream() + .map((value) -> coerceValue(specificType, value)) + .collect(Collectors.toList()); + } + catch (IllegalArgumentException ex) { + environment.getMessager().printMessage(Kind.ERROR, ex.getMessage(), + element, defaultValueAnnotation); + } + } + } + return null; + } + + private TypeMirror determineSpecificType(MetadataGenerationEnvironment environment) { + TypeMirror candidate = getSource().asType(); + TypeMirror elementCandidate = environment.getTypeUtils() + .extractElementType(candidate); + if (elementCandidate != null) { + candidate = elementCandidate; + } + PrimitiveType primitiveType = environment.getTypeUtils() + .getPrimitiveType(candidate); + return (primitiveType != null) ? primitiveType : candidate; + } + + private Object coerceValue(TypeMirror type, String value) { + Object coercedValue = type.accept(DefaultValueCoercionTypeVisitor.INSTANCE, + value); + return (coercedValue != null) ? coercedValue : value; + } + + private static class DefaultValueCoercionTypeVisitor + extends TypeKindVisitor8 { + + private static final DefaultValueCoercionTypeVisitor INSTANCE = new DefaultValueCoercionTypeVisitor(); + + private Integer parseInteger(String value) { + try { + return Integer.valueOf(value); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException( + String.format("Invalid number representation '%s'", value)); + } + } + + private Double parseFloatingPoint(String value) { + try { + return Double.valueOf(value); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException(String + .format("Invalid floating point representation '%s'", value)); + } + } + + @Override + public Object visitPrimitiveAsBoolean(PrimitiveType t, String value) { + return Boolean.parseBoolean(value); + } + + @Override + public Object visitPrimitiveAsByte(PrimitiveType t, String value) { + return parseInteger(value); + } + + @Override + public Object visitPrimitiveAsShort(PrimitiveType t, String value) { + return parseInteger(value); + } + + @Override + public Object visitPrimitiveAsInt(PrimitiveType t, String value) { + return parseInteger(value); + } + + @Override + public Object visitPrimitiveAsLong(PrimitiveType t, String value) { + return parseInteger(value); + } + + @Override + public Object visitPrimitiveAsChar(PrimitiveType t, String value) { + if (value.length() > 1) { + throw new IllegalArgumentException( + String.format("Invalid character representation '%s'", value)); + } + return value; + } + + @Override + public Object visitPrimitiveAsFloat(PrimitiveType t, String value) { + return parseFloatingPoint(value); + } + + @Override + public Object visitPrimitiveAsDouble(PrimitiveType t, String value) { + return parseFloatingPoint(value); + } + + } + + private static class DefaultPrimitiveTypeVisitor + extends TypeKindVisitor8 { + + private static final DefaultPrimitiveTypeVisitor INSTANCE = new DefaultPrimitiveTypeVisitor(); + + @Override + public Object visitPrimitiveAsBoolean(PrimitiveType t, Void ignore) { + return false; + } + + @Override + public Object visitPrimitiveAsByte(PrimitiveType t, Void ignore) { + return 0; + } + + @Override + public Object visitPrimitiveAsShort(PrimitiveType t, Void ignore) { + return 0; + } + + @Override + public Object visitPrimitiveAsInt(PrimitiveType t, Void ignore) { + return 0; + } + + @Override + public Object visitPrimitiveAsLong(PrimitiveType t, Void ignore) { + return 0L; + } + + @Override + public Object visitPrimitiveAsChar(PrimitiveType t, Void ignore) { + return null; + } + + @Override + public Object visitPrimitiveAsFloat(PrimitiveType t, Void ignore) { + return 0; + } + + @Override + public Object visitPrimitiveAsDouble(PrimitiveType t, Void ignore) { + return 0D; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java index 2eda08d412..80d21421f3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java @@ -41,4 +41,9 @@ class JavaBeanPropertyDescriptor extends PropertyDescriptor { && (getSetter() != null || isCollection); } + @Override + protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) { + return environment.getFieldDefaultValue(getOwnerElement(), getName()); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java index b8910e956c..55e9f12cbe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java @@ -57,6 +57,11 @@ class LombokPropertyDescriptor extends PropertyDescriptor { return !env.isExcluded(getType()) && (hasSetter(env) || isCollection); } + @Override + protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) { + return environment.getFieldDefaultValue(getOwnerElement(), getName()); + } + @Override protected boolean isNested(MetadataGenerationEnvironment environment) { if (!hasLombokPublicAccessor(environment, true)) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index 7c11a5ce8a..ba3bef8d30 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -16,14 +16,18 @@ package org.springframework.boot.configurationprocessor; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; @@ -51,6 +55,8 @@ class MetadataGenerationEnvironment { private final Elements elements; + private final Messager messager; + private final FieldValuesParser fieldValuesParser; private final Map> defaultValues = new HashMap<>(); @@ -61,6 +67,8 @@ class MetadataGenerationEnvironment { private final String deprecatedConfigurationPropertyAnnotation; + private final String defaultValueAnnotation; + private final String endpointAnnotation; private final String readOperationAnnotation; @@ -68,15 +76,18 @@ class MetadataGenerationEnvironment { MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation, String nestedConfigurationPropertyAnnotation, - String deprecatedConfigurationPropertyAnnotation, String endpointAnnotation, + String deprecatedConfigurationPropertyAnnotation, + String defaultValueAnnotation, String endpointAnnotation, String readOperationAnnotation) { this.typeExcludes = determineTypeExcludes(); this.typeUtils = new TypeUtils(environment); this.elements = environment.getElementUtils(); + this.messager = environment.getMessager(); this.fieldValuesParser = resolveFieldValuesParser(environment); this.configurationPropertiesAnnotation = configurationPropertiesAnnotation; this.nestedConfigurationPropertyAnnotation = nestedConfigurationPropertyAnnotation; this.deprecatedConfigurationPropertyAnnotation = deprecatedConfigurationPropertyAnnotation; + this.defaultValueAnnotation = defaultValueAnnotation; this.endpointAnnotation = endpointAnnotation; this.readOperationAnnotation = readOperationAnnotation; } @@ -112,7 +123,18 @@ class MetadataGenerationEnvironment { return this.typeUtils; } - public Object getDefaultValue(TypeElement type, String name) { + public Messager getMessager() { + return this.messager; + } + + /** + * Return the default value of the field with the specified {@code name}. + * @param type the type to consider + * @param name the name of the field + * @return the default value or {@code null} if the field does not exist or no default + * value has been detected + */ + public Object getFieldDefaultValue(TypeElement type, String name) { return this.defaultValues.computeIfAbsent(type, this::resolveFieldValues) .get(name); } @@ -171,10 +193,21 @@ class MetadataGenerationEnvironment { public Map getAnnotationElementValues(AnnotationMirror annotation) { Map values = new LinkedHashMap<>(); annotation.getElementValues().forEach((name, value) -> values - .put(name.getSimpleName().toString(), value.getValue())); + .put(name.getSimpleName().toString(), getAnnotationValue(value))); return values; } + private Object getAnnotationValue(AnnotationValue annotationValue) { + Object value = annotationValue.getValue(); + if (value instanceof List) { + List values = new ArrayList<>(); + ((List) value) + .forEach((v) -> values.add(((AnnotationValue) v).getValue())); + return values; + } + return value; + } + public TypeElement getConfigurationPropertiesAnnotationElement() { return this.elements.getTypeElement(this.configurationPropertiesAnnotation); } @@ -187,6 +220,10 @@ class MetadataGenerationEnvironment { return getAnnotation(element, this.nestedConfigurationPropertyAnnotation); } + public AnnotationMirror getDefaultValueAnnotation(Element element) { + return getAnnotation(element, this.defaultValueAnnotation); + } + public TypeElement getEndpointAnnotationElement() { return this.elements.getTypeElement(this.endpointAnnotation); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java index 4dc0847849..9037278627 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java @@ -33,7 +33,7 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; * @param the type of the source element that determines the property * @author Stephane Nicoll */ -abstract class PropertyDescriptor { +abstract class PropertyDescriptor { private final TypeElement ownerElement; @@ -98,6 +98,9 @@ abstract class PropertyDescriptor { protected abstract boolean isProperty(MetadataGenerationEnvironment environment); + protected abstract Object resolveDefaultValue( + MetadataGenerationEnvironment environment); + protected ItemDeprecation resolveItemDeprecation( MetadataGenerationEnvironment environment) { boolean deprecated = environment.isDeprecated(getGetter()) @@ -164,10 +167,6 @@ abstract class PropertyDescriptor { return environment.getTypeUtils().getJavaDoc(getField()); } - private Object resolveDefaultValue(MetadataGenerationEnvironment environment) { - return environment.getDefaultValue(getOwnerElement(), getName()); - } - private boolean isCyclePresent(Element returnType, Element element) { if (!(element.getEnclosingElement() instanceof TypeElement)) { return false; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index 433c2b71f3..2f05356514 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -16,13 +16,16 @@ package org.springframework.boot.configurationprocessor; -import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; /** * Resolve {@link PropertyDescriptor} instances. @@ -49,23 +52,59 @@ class PropertyDescriptorResolver { public Stream> resolve(TypeElement type, ExecutableElement factoryMethod) { TypeElementMembers members = new TypeElementMembers(this.environment, type); - List> candidates = new ArrayList<>(); + ExecutableElement constructor = resolveConstructor(type); + if (constructor != null) { + return resolveConstructorProperties(type, factoryMethod, members, + constructor); + } + else { + return resolveJavaBeanProperties(type, factoryMethod, members); + } + } + + public Stream> resolveConstructorProperties(TypeElement type, + ExecutableElement factoryMethod, TypeElementMembers members, + ExecutableElement constructor) { + Map> candidates = new LinkedHashMap<>(); + constructor.getParameters().forEach((parameter) -> { + String name = parameter.getSimpleName().toString(); + TypeMirror propertyType = parameter.asType(); + ExecutableElement getter = members.getPublicGetter(name, propertyType); + ExecutableElement setter = members.getPublicSetter(name, propertyType); + VariableElement field = members.getFields().get(name); + register(candidates, new ConstructorParameterPropertyDescriptor(type, + factoryMethod, parameter, name, propertyType, field, getter, setter)); + }); + return candidates.values().stream(); + } + + public Stream> resolveJavaBeanProperties(TypeElement type, + ExecutableElement factoryMethod, TypeElementMembers members) { // First check if we have regular java bean properties there + Map> candidates = new LinkedHashMap<>(); members.getPublicGetters().forEach((name, getter) -> { - TypeMirror returnType = getter.getReturnType(); - candidates.add(new JavaBeanPropertyDescriptor(type, factoryMethod, getter, - name, returnType, members.getFields().get(name), - members.getPublicSetter(name, returnType))); + TypeMirror propertyType = getter.getReturnType(); + register(candidates, + new JavaBeanPropertyDescriptor(type, factoryMethod, getter, name, + propertyType, members.getFields().get(name), + members.getPublicSetter(name, propertyType))); }); // Then check for Lombok ones members.getFields().forEach((name, field) -> { - TypeMirror returnType = field.asType(); - ExecutableElement getter = members.getPublicGetter(name, returnType); - ExecutableElement setter = members.getPublicSetter(name, returnType); - candidates.add(new LombokPropertyDescriptor(type, factoryMethod, field, name, - returnType, getter, setter)); + TypeMirror propertyType = field.asType(); + ExecutableElement getter = members.getPublicGetter(name, propertyType); + ExecutableElement setter = members.getPublicSetter(name, propertyType); + register(candidates, new LombokPropertyDescriptor(type, factoryMethod, field, + name, propertyType, getter, setter)); }); - return candidates.stream().filter(this::isCandidate); + return candidates.values().stream(); + } + + private void register(Map> candidates, + PropertyDescriptor descriptor) { + if (!candidates.containsKey(descriptor.getName()) && isCandidate(descriptor)) { + candidates.put(descriptor.getName(), descriptor); + } } private boolean isCandidate(PropertyDescriptor descriptor) { @@ -73,4 +112,13 @@ class PropertyDescriptorResolver { || descriptor.isNested(this.environment); } + private ExecutableElement resolveConstructor(TypeElement type) { + List constructors = ElementFilter + .constructorsIn(type.getEnclosedElements()); + if (constructors.size() == 1 && constructors.get(0).getParameters().size() > 0) { + return constructors.get(0); + } + return null; + } + } 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 c3c4275642..8f52026a72 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 @@ -142,6 +142,42 @@ class TypeUtils { return type.accept(this.typeExtractor, createTypeDescriptor(element)); } + /** + * Extract the target element type from the specified container type or {@code null} + * if no element type was found. + * @param type a type, potentially wrapping an element type + * @return the element type or {@code null} if no specific type was found + */ + public TypeMirror extractElementType(TypeMirror type) { + if (!this.env.getTypeUtils().isAssignable(type, this.collectionType)) { + return null; + } + return getCollectionElementType(type); + } + + private TypeMirror getCollectionElementType(TypeMirror type) { + if (((TypeElement) this.types.asElement(type)).getQualifiedName() + .contentEquals(Collection.class.getName())) { + DeclaredType declaredType = (DeclaredType) type; + // raw type, just "Collection" + if (declaredType.getTypeArguments().size() == 0) { + return this.types.getDeclaredType(this.env.getElementUtils() + .getTypeElement(Object.class.getName())); + } + else { // return type argument to Collection<...> + return declaredType.getTypeArguments().get(0); + } + } + + // recursively walk the supertypes, looking for Collection<...> + for (TypeMirror superType : this.env.getTypeUtils().directSupertypes(type)) { + if (this.types.isAssignable(superType, this.collectionType)) { + return getCollectionElementType(superType); + } + } + return null; + } + public boolean isCollectionOrMap(TypeMirror type) { return this.env.getTypeUtils().isAssignable(type, this.collectionType) || this.env.getTypeUtils().isAssignable(type, this.mapType); @@ -156,6 +192,19 @@ class TypeUtils { return "".equals(javadoc) ? null : javadoc; } + /** + * Return the {@link PrimitiveType} of the specified type or {@code null} if the type + * does not represent a valid wrapper type. + * @param typeMirror a type + * @return the primitive type or {@code null} if the type is not a wrapper type + */ + public PrimitiveType getPrimitiveType(TypeMirror typeMirror) { + if (getPrimitiveFor(typeMirror) != null) { + return this.types.unboxedType(typeMirror); + } + return null; + } + public TypeMirror getWrapperOrPrimitiveFor(TypeMirror typeMirror) { Class candidate = getWrapperFor(typeMirror); if (candidate != null) { 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 91dab0fbdd..c596ab4d09 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 @@ -43,6 +43,9 @@ import org.springframework.boot.configurationsample.specific.InnerClassHierarchi import org.springframework.boot.configurationsample.specific.InnerClassProperties; import org.springframework.boot.configurationsample.specific.InnerClassRootConfig; import org.springframework.boot.configurationsample.specific.InvalidAccessorProperties; +import org.springframework.boot.configurationsample.specific.InvalidDefaultValueCharacterProperties; +import org.springframework.boot.configurationsample.specific.InvalidDefaultValueFloatingPointProperties; +import org.springframework.boot.configurationsample.specific.InvalidDefaultValueNumberProperties; import org.springframework.boot.configurationsample.specific.InvalidDoubleRegistrationProperties; import org.springframework.boot.configurationsample.specific.SimplePojo; import org.springframework.boot.configurationsample.specific.StaticAccessor; @@ -385,4 +388,26 @@ public class ConfigurationMetadataAnnotationProcessorTests .withMessageContaining("Compilation failed"); } + @Test + public void constructorParameterPropertyWithInvalidDefaultValueOnNumber() { + assertThatIllegalStateException() + .isThrownBy(() -> compile(InvalidDefaultValueNumberProperties.class)) + .withMessageContaining("Compilation failed"); + } + + @Test + public void constructorParameterPropertyWithInvalidDefaultValueOnFloatingPoint() { + assertThatIllegalStateException() + .isThrownBy( + () -> compile(InvalidDefaultValueFloatingPointProperties.class)) + .withMessageContaining("Compilation failed"); + } + + @Test + public void constructorParameterPropertyWithInvalidDefaultValueOnCharacter() { + assertThatIllegalStateException() + .isThrownBy(() -> compile(InvalidDefaultValueCharacterProperties.class)) + .withMessageContaining("Compilation failed"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java new file mode 100644 index 0000000000..2242c0ae0f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java @@ -0,0 +1,318 @@ +/* + * 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.configurationprocessor; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.ElementFilter; + +import org.junit.Test; + +import org.springframework.boot.configurationsample.immutable.ImmutableCollectionProperties; +import org.springframework.boot.configurationsample.immutable.ImmutableInnerClassProperties; +import org.springframework.boot.configurationsample.immutable.ImmutablePrimitiveProperties; +import org.springframework.boot.configurationsample.immutable.ImmutablePrimitiveWithDefaultsProperties; +import org.springframework.boot.configurationsample.immutable.ImmutablePrimitiveWrapperWithDefaultsProperties; +import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConstructorParameterPropertyDescriptor}. + * + * @author Stephane Nicoll + */ +public class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTests { + + @Test + public void constructorParameterSimpleProperty() throws IOException { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableSimpleProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "theName"); + assertThat(property.getName()).isEqualTo("theName"); + assertThat(property.getSource()).hasToString("theName"); + assertThat(property.getGetter().getSimpleName()).hasToString("getTheName"); + assertThat(property.isProperty(metadataEnv)).isTrue(); + assertThat(property.isNested(metadataEnv)).isFalse(); + }); + } + + @Test + public void constructorParameterNestedPropertySameClass() throws IOException { + process(ImmutableInnerClassProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableInnerClassProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "first"); + assertThat(property.getName()).isEqualTo("first"); + assertThat(property.getSource()).hasToString("first"); + assertThat(property.getGetter().getSimpleName()).hasToString("getFirst"); + assertThat(property.isProperty(metadataEnv)).isFalse(); + assertThat(property.isNested(metadataEnv)).isTrue(); + }); + } + + @Test + public void constructorParameterNestedPropertyWithAnnotation() throws IOException { + process(ImmutableInnerClassProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableInnerClassProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "third"); + assertThat(property.getName()).isEqualTo("third"); + assertThat(property.getSource()).hasToString("third"); + assertThat(property.getGetter().getSimpleName()).hasToString("getThird"); + assertThat(property.isProperty(metadataEnv)).isFalse(); + assertThat(property.isNested(metadataEnv)).isTrue(); + }); + } + + @Test + public void constructorParameterSimplePropertyWithNoAccessorShouldBeExposed() + throws IOException { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableSimpleProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "counter"); + assertThat(property.getName()).isEqualTo("counter"); + assertThat(property.getSource()).hasToString("counter"); + assertThat(property.getGetter()).isNull(); + assertThat(property.isProperty(metadataEnv)).isTrue(); + assertThat(property.isNested(metadataEnv)).isFalse(); + }); + } + + @Test + public void constructorParameterMetadataSimpleProperty() throws IOException { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableSimpleProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "counter"); + assertItemMetadata(metadataEnv, property).isProperty().hasName("test.counter") + .hasType(Long.class).hasSourceType(ImmutableSimpleProperties.class) + .hasNoDescription().isNotDeprecated(); + }); + } + + @Test + public void constructorParameterMetadataNestedGroup() throws IOException { + process(ImmutableInnerClassProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableInnerClassProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "first"); + assertItemMetadata(metadataEnv, property).isGroup().hasName("test.first") + .hasType( + "org.springframework.boot.configurationsample.immutable.ImmutableInnerClassProperties$Foo") + .hasSourceType(ImmutableInnerClassProperties.class) + .hasSourceMethod("getFirst()").hasNoDescription().isNotDeprecated(); + }); + } + + @Test + public void constructorParameterDeprecatedPropertyOnGetter() throws IOException { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableSimpleProperties.class); + ExecutableElement getter = getMethod(ownerElement, "isFlag"); + VariableElement field = getField(ownerElement, "flag"); + VariableElement constructorParameter = getConstructorParameter(ownerElement, + "flag"); + ConstructorParameterPropertyDescriptor property = new ConstructorParameterPropertyDescriptor( + ownerElement, null, constructorParameter, "flag", field.asType(), + field, getter, null); + assertItemMetadata(metadataEnv, property).isProperty() + .isDeprecatedWithNoInformation(); + }); + } + + @Test + public void constructorParameterPropertyWithDescription() throws IOException { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableSimpleProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "theName"); + assertItemMetadata(metadataEnv, property).isProperty() + .hasDescription("The name of this simple properties."); + }); + } + + @Test + public void constructorParameterPropertyWithDefaultValue() throws IOException { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableSimpleProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor( + ownerElement, "theName"); + assertItemMetadata(metadataEnv, property).isProperty() + .hasDefaultValue("boot"); + }); + } + + @Test + public void constructorParameterPropertyWithPrimitiveTypes() throws IOException { + process(ImmutablePrimitiveProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutablePrimitiveProperties.class); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "flag")) + .hasDefaultValue(false); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "octet")).hasDefaultValue(0); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "letter")) + .hasDefaultValue(null); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "number")).hasDefaultValue(0); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "counter")).hasDefaultValue(0); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "value")).hasDefaultValue(0L); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "percentage")) + .hasDefaultValue(0); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "ratio")).hasDefaultValue(0D); + }); + } + + @Test + public void constructorParameterPropertyWithPrimitiveTypesAndDefaultValues() + throws IOException { + process(ImmutablePrimitiveWithDefaultsProperties.class, + (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement( + ImmutablePrimitiveWithDefaultsProperties.class); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "flag")) + .hasDefaultValue(true); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "octet")) + .hasDefaultValue(120); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "letter")) + .hasDefaultValue("a"); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "number")) + .hasDefaultValue(1000); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "counter")) + .hasDefaultValue(42); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "value")) + .hasDefaultValue(2000); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "percentage")) + .hasDefaultValue(0.5); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "ratio")) + .hasDefaultValue(42.42); + }); + } + + @Test + public void constructorParameterPropertyWithPrimitiveWrapperTypesAndDefaultValues() + throws IOException { + process(ImmutablePrimitiveWrapperWithDefaultsProperties.class, + (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement( + ImmutablePrimitiveWrapperWithDefaultsProperties.class); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "flag")) + .hasDefaultValue(true); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "octet")) + .hasDefaultValue(120); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "letter")) + .hasDefaultValue("a"); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "number")) + .hasDefaultValue(1000); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "counter")) + .hasDefaultValue(42); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "value")) + .hasDefaultValue(2000); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "percentage")) + .hasDefaultValue(0.5); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "ratio")) + .hasDefaultValue(42.42); + }); + } + + @Test + public void constructorParameterPropertyWithCollectionTypesAndDefaultValues() + throws IOException { + process(ImmutableCollectionProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv + .getRootElement(ImmutableCollectionProperties.class); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "names")) + .hasDefaultValue(null); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "flags")) + .hasDefaultValue(Arrays.asList(true, false)); + assertItemMetadata(metadataEnv, + createPropertyDescriptor(ownerElement, "durations")) + .hasDefaultValue(Arrays.asList("10s", "1m", "1h")); + }); + } + + protected ConstructorParameterPropertyDescriptor createPropertyDescriptor( + TypeElement ownerElement, String name) { + VariableElement constructorParameter = getConstructorParameter(ownerElement, + name); + VariableElement field = getField(ownerElement, name); + ExecutableElement getter = getMethod(ownerElement, + createAccessorMethodName("get", name)); + ExecutableElement setter = getMethod(ownerElement, + createAccessorMethodName("set", name)); + return new ConstructorParameterPropertyDescriptor(ownerElement, null, + constructorParameter, name, field.asType(), field, getter, setter); + } + + private VariableElement getConstructorParameter(TypeElement ownerElement, + String name) { + List constructors = ElementFilter + .constructorsIn(ownerElement.getEnclosedElements()).stream() + .filter((constructor) -> !constructor.getParameters().isEmpty()) + .collect(Collectors.toList()); + if (constructors.size() != 1) { + throw new IllegalStateException( + "No candidate constructor for " + ownerElement); + } + return constructors.get(0).getParameters().stream() + .filter((parameter) -> parameter.getSimpleName().toString().equals(name)) + .findFirst().orElse(null); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutablePropertiesMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutablePropertiesMetadataGenerationTests.java new file mode 100644 index 0000000000..0b7fb04b72 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutablePropertiesMetadataGenerationTests.java @@ -0,0 +1,52 @@ +/* + * 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.configurationprocessor; + +import org.junit.Test; + +import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; +import org.springframework.boot.configurationprocessor.metadata.Metadata; +import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Metadata generation tests for immutable properties. + * + * @author Stephane Nicoll + */ +public class ImmutablePropertiesMetadataGenerationTests + extends AbstractMetadataGenerationTests { + + @Test + public void immutableSimpleProperties() { + ConfigurationMetadata metadata = compile(ImmutableSimpleProperties.class); + assertThat(metadata).has(Metadata.withGroup("immutable") + .fromSource(ImmutableSimpleProperties.class)); + assertThat(metadata).has(Metadata.withProperty("immutable.the-name", String.class) + .fromSource(ImmutableSimpleProperties.class) + .withDescription("The name of this simple properties.") + .withDefaultValue("boot")); + assertThat(metadata).has(Metadata.withProperty("immutable.flag", Boolean.class) + .withDefaultValue(false).fromSource(ImmutableSimpleProperties.class) + .withDescription("A simple flag.").withDeprecation(null, null)); + assertThat(metadata).has(Metadata.withProperty("immutable.comparator")); + assertThat(metadata).has(Metadata.withProperty("immutable.counter")); + assertThat(metadata.getItems()).hasSize(5); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java index 8f666e30f8..d86f2b6311 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java @@ -36,6 +36,7 @@ class MetadataGenerationEnvironmentFactory TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION, TestConfigurationMetadataAnnotationProcessor.NESTED_CONFIGURATION_PROPERTY_ANNOTATION, TestConfigurationMetadataAnnotationProcessor.DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.DEFAULT_VALUE_ANNOTATION, TestConfigurationMetadataAnnotationProcessor.ENDPOINT_ANNOTATION, TestConfigurationMetadataAnnotationProcessor.READ_OPERATION_ANNOTATION); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java index 4aea72ccc0..22e83e81c2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java @@ -34,6 +34,7 @@ import org.junit.rules.TemporaryFolder; import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; import org.springframework.boot.configurationprocessor.test.RoundEnvironmentTester; import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor; +import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties; import org.springframework.boot.configurationsample.lombok.LombokExplicitProperties; import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties; import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties; @@ -41,6 +42,7 @@ import org.springframework.boot.configurationsample.simple.HierarchicalPropertie import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesGrandparent; import org.springframework.boot.configurationsample.simple.HierarchicalPropertiesParent; import org.springframework.boot.configurationsample.simple.SimpleProperties; +import org.springframework.boot.configurationsample.specific.TwoConstructorsExample; import org.springframework.boot.testsupport.compiler.TestCompiler; import static org.assertj.core.api.Assertions.assertThat; @@ -100,6 +102,21 @@ public class PropertyDescriptorResolverTests { "description", "counter", "number", "items"))); } + @Test + public void propertiesWithConstructorParameters() throws IOException { + process(ImmutableSimpleProperties.class, + propertyNames((stream) -> assertThat(stream).containsExactly("theName", + "flag", "comparator", "counter"))); + } + + @Test + public void propertiesWithSeveralConstructors() throws IOException { + process(TwoConstructorsExample.class, + propertyNames((stream) -> assertThat(stream).containsExactly("name"))); + process(TwoConstructorsExample.class, properties((stream) -> assertThat(stream) + .element(0).isInstanceOf(JavaBeanPropertyDescriptor.class))); + } + private BiConsumer properties( Consumer>> stream) { return (element, metadataEnv) -> { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java index e8fbac8aa8..f311617f99 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java @@ -47,6 +47,8 @@ public class TestConfigurationMetadataAnnotationProcessor public static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.configurationsample.DeprecatedConfigurationProperty"; + public static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.configurationsample.DefaultValue"; + public static final String ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.Endpoint"; public static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.configurationsample.ReadOperation"; @@ -74,6 +76,11 @@ public class TestConfigurationMetadataAnnotationProcessor return DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION; } + @Override + protected String defaultValueAnnotation() { + return DEFAULT_VALUE_ANNOTATION; + } + @Override protected String endpointAnnotation() { return ENDPOINT_ANNOTATION; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DefaultValue.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DefaultValue.java new file mode 100644 index 0000000000..32604a4621 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DefaultValue.java @@ -0,0 +1,38 @@ +/* + * 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code @DefaultValue} for testing (removes the need for a + * dependency on the real annotation). + * + * @author Stephane Nicoll + */ +@Target({ ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DefaultValue { + + String[] value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableCollectionProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableCollectionProperties.java new file mode 100644 index 0000000000..3b7f4413e2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableCollectionProperties.java @@ -0,0 +1,45 @@ +/* + * 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.immutable; + +import java.time.Duration; +import java.util.List; + +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Simple immutable properties with collections types and defaults. + * + * @author Stephane Nicoll + */ +public class ImmutableCollectionProperties { + + private final List names; + + private final List flags; + + private final List durations; + + public ImmutableCollectionProperties(List names, + @DefaultValue({ "true", "false" }) List flags, + @DefaultValue({ "10s", "1m", "1h" }) List durations) { + this.names = names; + this.flags = flags; + this.durations = durations; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableInnerClassProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableInnerClassProperties.java new file mode 100644 index 0000000000..d25b11b6d3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableInnerClassProperties.java @@ -0,0 +1,106 @@ +/* + * 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.immutable; + +import org.springframework.boot.configurationsample.NestedConfigurationProperty; +import org.springframework.boot.configurationsample.specific.SimplePojo; + +/** + * Inner properties, in immutable format. + * + * @author Stephane Nicoll + */ +public class ImmutableInnerClassProperties { + + private final Foo first; + + private Foo second; + + @NestedConfigurationProperty + private final SimplePojo third; + + private final Fourth fourth; + + public ImmutableInnerClassProperties(Foo first, Foo second, SimplePojo third, + Fourth fourth) { + this.first = first; + this.second = second; + this.third = third; + this.fourth = fourth; + } + + public Foo getFirst() { + return this.first; + } + + public Foo getTheSecond() { + return this.second; + } + + public void setTheSecond(Foo second) { + this.second = second; + } + + public SimplePojo getThird() { + return this.third; + } + + public Fourth getFourth() { + return this.fourth; + } + + public static class Foo { + + private String name; + + private final Bar bar = new Bar(); + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Bar getBar() { + return this.bar; + } + + public static class Bar { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + } + + public enum Fourth { + + YES, NO + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveProperties.java new file mode 100644 index 0000000000..24a6961374 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveProperties.java @@ -0,0 +1,55 @@ +/* + * 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.immutable; + +/** + * Simple immutable properties with primitive types. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("unused") +public class ImmutablePrimitiveProperties { + + private final boolean flag; + + private final byte octet; + + private final char letter; + + private final short number; + + private final int counter; + + private final long value; + + private final float percentage; + + private final double ratio; + + public ImmutablePrimitiveProperties(boolean flag, byte octet, char letter, + short number, int counter, long value, float percentage, double ratio) { + this.flag = flag; + this.octet = octet; + this.letter = letter; + this.number = number; + this.counter = counter; + this.value = value; + this.percentage = percentage; + this.ratio = ratio; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveWithDefaultsProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveWithDefaultsProperties.java new file mode 100644 index 0000000000..c496f0942d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveWithDefaultsProperties.java @@ -0,0 +1,60 @@ +/* + * 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.immutable; + +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Simple immutable properties with primitive types and defaults. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("unused") +public class ImmutablePrimitiveWithDefaultsProperties { + + private final boolean flag; + + private final byte octet; + + private final char letter; + + private final short number; + + private final int counter; + + private final long value; + + private final float percentage; + + private final double ratio; + + public ImmutablePrimitiveWithDefaultsProperties(@DefaultValue("true") boolean flag, + @DefaultValue("120") byte octet, @DefaultValue("a") char letter, + @DefaultValue("1000") short number, @DefaultValue("42") int counter, + @DefaultValue("2000") long value, @DefaultValue("0.5") float percentage, + @DefaultValue("42.42") double ratio) { + this.flag = flag; + this.octet = octet; + this.letter = letter; + this.number = number; + this.counter = counter; + this.value = value; + this.percentage = percentage; + this.ratio = ratio; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveWrapperWithDefaultsProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveWrapperWithDefaultsProperties.java new file mode 100644 index 0000000000..c2148f93e3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutablePrimitiveWrapperWithDefaultsProperties.java @@ -0,0 +1,60 @@ +/* + * 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.immutable; + +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Simple immutable properties with primitive wrapper types and defaults. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("unused") +public class ImmutablePrimitiveWrapperWithDefaultsProperties { + + private final Boolean flag; + + private final Byte octet; + + private final Character letter; + + private final Short number; + + private final Integer counter; + + private final Long value; + + private final Float percentage; + + private final Double ratio; + + public ImmutablePrimitiveWrapperWithDefaultsProperties( + @DefaultValue("true") Boolean flag, @DefaultValue("120") Byte octet, + @DefaultValue("a") Character letter, @DefaultValue("1000") Short number, + @DefaultValue("42") Integer counter, @DefaultValue("2000") Long value, + @DefaultValue("0.5") Float percentage, @DefaultValue("42.42") Double ratio) { + this.flag = flag; + this.octet = octet; + this.letter = letter; + this.number = number; + this.counter = counter; + this.value = value; + this.percentage = percentage; + this.ratio = ratio; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableSimpleProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableSimpleProperties.java new file mode 100644 index 0000000000..b55b0c3ade --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableSimpleProperties.java @@ -0,0 +1,71 @@ +/* + * 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.immutable; + +import java.util.Comparator; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Simple properties, in immutable format. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("immutable") +public class ImmutableSimpleProperties { + + /** + * The name of this simple properties. + */ + private final String theName; + + /** + * A simple flag. + */ + private final boolean flag; + + // An interface can still be injected because it might have a converter + private final Comparator comparator; + + // Even if it is not exposed, we're still offering a way to bind the value via the + // constructor so it should be present in the metadata + @SuppressWarnings("unused") + private final Long counter; + + public ImmutableSimpleProperties(@DefaultValue("boot") String theName, boolean flag, + Comparator comparator, Long counter) { + this.theName = theName; + this.flag = flag; + this.comparator = comparator; + this.counter = counter; + } + + public String getTheName() { + return this.theName; + } + + @Deprecated + public boolean isFlag() { + return this.flag; + } + + public Comparator getComparator() { + return this.comparator; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueCharacterProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueCharacterProperties.java new file mode 100644 index 0000000000..b16d854036 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueCharacterProperties.java @@ -0,0 +1,40 @@ +/* + * 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.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Demonstrates that an invalid default character value leads to a compilation failure. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("test") +public class InvalidDefaultValueCharacterProperties { + + private final char letter; + + public InvalidDefaultValueCharacterProperties(@DefaultValue("bad") char letter) { + this.letter = letter; + } + + public char getLetter() { + return this.letter; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueFloatingPointProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueFloatingPointProperties.java new file mode 100644 index 0000000000..33862006e6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueFloatingPointProperties.java @@ -0,0 +1,42 @@ +/* + * 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.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Demonstrates that an invalid default floating point value leads to a compilation + * failure. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("test") +public class InvalidDefaultValueFloatingPointProperties { + + private final Double ratio; + + public InvalidDefaultValueFloatingPointProperties( + @DefaultValue("55.55.33") Double ratio) { + this.ratio = ratio; + } + + public Double getRatio() { + return this.ratio; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueNumberProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueNumberProperties.java new file mode 100644 index 0000000000..c2747b72f7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InvalidDefaultValueNumberProperties.java @@ -0,0 +1,40 @@ +/* + * 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.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Demonstrates that an invalid default number value leads to a compilation failure. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("test") +public class InvalidDefaultValueNumberProperties { + + private final int counter; + + public InvalidDefaultValueNumberProperties(@DefaultValue("invalid") int counter) { + this.counter = counter; + } + + public int getCounter() { + return this.counter; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsExample.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsExample.java new file mode 100644 index 0000000000..f892aceea0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsExample.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.specific; + +/** + * A type with more than one constructor. + * + * @author Stephane Nicoll + */ +public class TwoConstructorsExample { + + private String name; + + public TwoConstructorsExample() { + } + + public TwoConstructorsExample(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 858ed62c32..6493fdf388 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -13,6 +13,7 @@ +