diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc index 485ce1b3074..80eac6fd96c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc @@ -255,6 +255,8 @@ include-code::AcmeProperties[] NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON. +If you use `@ConfigurationProperties` with record class then record components' descriptions should be provided via class-level Javadoc tag `@param` (there are no explicit instance fields in record classes to put regular field-level Javadocs on). + Here are some rules we follow internally to make sure descriptions are consistent: * Do not start the description by "The" or "A". diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc index d7504c81c51..6276bae645e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc @@ -89,6 +89,8 @@ The Javadoc on fields is used to populate the `description` attribute. For insta NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON. +If you use `@ConfigurationProperties` with record class then record components' descriptions should be provided via class-level Javadoc tag `@param` (there are no explicit instance fields in record classes to put regular field-level Javadocs on). + The annotation processor applies a number of heuristics to extract the default value from the source model. Default values have to be provided statically. In particular, do not refer to a constant defined in another class. Also, the annotation processor cannot auto-detect default values for ``Enum``s and ``Collections``s. 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 index a567a946585..da36d12a31d 100644 --- 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 @@ -16,195 +16,47 @@ package org.springframework.boot.configurationprocessor; +import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.function.Function; -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 + * @author Phillip Webb */ -class ConstructorParameterPropertyDescriptor extends PropertyDescriptor { +class ConstructorParameterPropertyDescriptor extends ParameterPropertyDescriptor { - 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); - } + private final ExecutableElement 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); - } + private final VariableElement field; - @Override - protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) { - Object defaultValue = getDefaultValueFromAnnotation(environment, getSource()); - if (defaultValue != null) { - return defaultValue; - } - return getSource().asType().accept(DefaultPrimitiveTypeVisitor.INSTANCE, null); + ConstructorParameterPropertyDescriptor(String name, TypeMirror type, VariableElement parameter, + TypeElement declaringElement, ExecutableElement getter, ExecutableElement setter, VariableElement field) { + super(name, type, parameter, declaringElement, getter); + this.setter = setter; + this.field = field; } - private Object getDefaultValueFromAnnotation(MetadataGenerationEnvironment environment, Element element) { - AnnotationMirror annotation = environment.getDefaultValueAnnotation(element); - List defaultValue = getDefaultValue(environment, annotation); - 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)).toList(); - } - catch (IllegalArgumentException ex) { - environment.getMessager().printMessage(Kind.ERROR, ex.getMessage(), element, annotation); - } - } - return null; - } - - @SuppressWarnings("unchecked") - private List getDefaultValue(MetadataGenerationEnvironment environment, AnnotationMirror annotation) { - if (annotation == null) { - return null; - } - Map values = environment.getAnnotationElementValues(annotation); - return (List) values.get("value"); - } - - 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; + @Override + protected List getDeprecatableElements() { + return Arrays.asList(getGetter(), this.setter, this.field); } - private static final class DefaultValueCoercionTypeVisitor extends TypeKindVisitor8 { - - private static final DefaultValueCoercionTypeVisitor INSTANCE = new DefaultValueCoercionTypeVisitor(); - - private T parseNumber(String value, Function parser, - PrimitiveType primitiveType) { - try { - return parser.apply(value); - } - catch (NumberFormatException ex) { - throw new IllegalArgumentException( - String.format("Invalid %s representation '%s'", primitiveType, value)); - } - } - - @Override - public Object visitPrimitiveAsBoolean(PrimitiveType t, String value) { - return Boolean.parseBoolean(value); - } - - @Override - public Object visitPrimitiveAsByte(PrimitiveType t, String value) { - return parseNumber(value, Byte::parseByte, t); - } - - @Override - public Object visitPrimitiveAsShort(PrimitiveType t, String value) { - return parseNumber(value, Short::parseShort, t); - } - - @Override - public Object visitPrimitiveAsInt(PrimitiveType t, String value) { - return parseNumber(value, Integer::parseInt, t); - } - - @Override - public Object visitPrimitiveAsLong(PrimitiveType t, String value) { - return parseNumber(value, Long::parseLong, t); - } - - @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 parseNumber(value, Float::parseFloat, t); - } - - @Override - public Object visitPrimitiveAsDouble(PrimitiveType t, String value) { - return parseNumber(value, Double::parseDouble, t); - } - + @Override + protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) { + return environment.getNestedConfigurationPropertyAnnotation(this.field) != null; } - private static final 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 (byte) 0; - } - - @Override - public Object visitPrimitiveAsShort(PrimitiveType t, Void ignore) { - return (short) 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 0F; - } - - @Override - public Object visitPrimitiveAsDouble(PrimitiveType t, Void ignore) { - return 0D; - } - + @Override + protected String resolveDescription(MetadataGenerationEnvironment environment) { + return environment.getTypeUtils().getJavaDoc(this.field); } } 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 4fcf11366b4..5c98c3fbdd4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 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. @@ -16,6 +16,10 @@ package org.springframework.boot.configurationprocessor; +import java.util.Arrays; +import java.util.List; + +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -25,23 +29,54 @@ import javax.lang.model.type.TypeMirror; * A {@link PropertyDescriptor} for a standard JavaBean property. * * @author Stephane Nicoll + * @author Phillip Webb */ -class JavaBeanPropertyDescriptor extends PropertyDescriptor { +class JavaBeanPropertyDescriptor extends PropertyDescriptor { + + private final ExecutableElement setter; + + private final VariableElement field; - JavaBeanPropertyDescriptor(TypeElement ownerElement, ExecutableElement factoryMethod, ExecutableElement getter, - String name, TypeMirror type, VariableElement field, ExecutableElement setter) { - super(ownerElement, factoryMethod, getter, name, type, field, getter, setter); + private final ExecutableElement factoryMethod; + + JavaBeanPropertyDescriptor(String name, TypeMirror type, TypeElement declaringElement, ExecutableElement getter, + ExecutableElement setter, VariableElement field, ExecutableElement factoryMethod) { + super(name, type, declaringElement, getter); + this.setter = setter; + this.field = field; + this.factoryMethod = factoryMethod; + } + + ExecutableElement getSetter() { + return this.setter; } @Override - protected boolean isProperty(MetadataGenerationEnvironment env) { - boolean isCollection = env.getTypeUtils().isCollectionOrMap(getType()); - return !env.isExcluded(getType()) && getGetter() != null && (getSetter() != null || isCollection); + protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) { + return environment.getNestedConfigurationPropertyAnnotation(this.field) != null; + } + + @Override + protected String resolveDescription(MetadataGenerationEnvironment environment) { + return environment.getTypeUtils().getJavaDoc(this.field); } @Override protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) { - return environment.getFieldDefaultValue(getOwnerElement(), getName()); + return environment.getFieldDefaultValue(getDeclaringElement(), getName()); + } + + @Override + protected List getDeprecatableElements() { + return Arrays.asList(getGetter(), this.setter, this.field, this.factoryMethod); + } + + @Override + public boolean isProperty(MetadataGenerationEnvironment env) { + boolean isCollection = env.getTypeUtils().isCollectionOrMap(getType()); + boolean hasGetter = getGetter() != null; + boolean hasSetter = getSetter() != null; + return !env.isExcluded(getType()) && hasGetter && (hasSetter || isCollection); } } 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 81a6d6b5a46..83a50b863d5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 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. @@ -16,23 +16,25 @@ package org.springframework.boot.configurationprocessor; +import java.util.Arrays; +import java.util.List; import java.util.Map; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; -import org.springframework.boot.configurationprocessor.metadata.ItemDeprecation; - /** * A {@link PropertyDescriptor} for a Lombok field. * * @author Stephane Nicoll + * @author Phillip Webb */ -class LombokPropertyDescriptor extends PropertyDescriptor { +class LombokPropertyDescriptor extends PropertyDescriptor { private static final String LOMBOK_DATA_ANNOTATION = "lombok.Data"; @@ -44,44 +46,62 @@ class LombokPropertyDescriptor extends PropertyDescriptor { private static final String LOMBOK_ACCESS_LEVEL_PUBLIC = "PUBLIC"; - LombokPropertyDescriptor(TypeElement typeElement, ExecutableElement factoryMethod, VariableElement field, - String name, TypeMirror type, ExecutableElement getter, ExecutableElement setter) { - super(typeElement, factoryMethod, field, name, type, field, getter, setter); + private final ExecutableElement setter; + + private final VariableElement field; + + private final ExecutableElement factoryMethod; + + LombokPropertyDescriptor(String name, TypeMirror type, TypeElement declaringElement, ExecutableElement getter, + ExecutableElement setter, VariableElement field, ExecutableElement factoryMethod) { + super(name, type, declaringElement, getter); + this.factoryMethod = factoryMethod; + this.field = field; + this.setter = setter; + } + + VariableElement getField() { + return this.field; } @Override - protected boolean isProperty(MetadataGenerationEnvironment env) { - if (!hasLombokPublicAccessor(env, true)) { - return false; - } - boolean isCollection = env.getTypeUtils().isCollectionOrMap(getType()); - return !env.isExcluded(getType()) && (hasSetter(env) || isCollection); + protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) { + return environment.getNestedConfigurationPropertyAnnotation(getField()) != null; + } + + @Override + protected String resolveDescription(MetadataGenerationEnvironment environment) { + return environment.getTypeUtils().getJavaDoc(this.field); } @Override protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) { - return environment.getFieldDefaultValue(getOwnerElement(), getName()); + return environment.getFieldDefaultValue(getDeclaringElement(), getName()); + } + + @Override + protected List getDeprecatableElements() { + return Arrays.asList(getGetter(), this.setter, this.field, this.factoryMethod); } @Override - protected boolean isNested(MetadataGenerationEnvironment environment) { - if (!hasLombokPublicAccessor(environment, true)) { + public boolean isProperty(MetadataGenerationEnvironment env) { + if (!hasLombokPublicAccessor(env, true)) { return false; } - return super.isNested(environment); + boolean isCollection = env.getTypeUtils().isCollectionOrMap(getType()); + return !env.isExcluded(getType()) && (hasSetter(env) || isCollection); } @Override - protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) { - boolean deprecated = environment.isDeprecated(getField()) || environment.isDeprecated(getGetter()) - || environment.isDeprecated(getFactoryMethod()); - return deprecated ? environment.resolveItemDeprecation(getGetter()) : null; + public boolean isNested(MetadataGenerationEnvironment environment) { + return hasLombokPublicAccessor(environment, true) && super.isNested(environment); } private boolean hasSetter(MetadataGenerationEnvironment env) { boolean nonFinalPublicField = !getField().getModifiers().contains(Modifier.FINAL) && hasLombokPublicAccessor(env, false); - return getSetter() != null || nonFinalPublicField; + return this.setter != null || nonFinalPublicField; } /** @@ -98,12 +118,12 @@ class LombokPropertyDescriptor extends PropertyDescriptor { if (lombokMethodAnnotationOnField != null) { return isAccessLevelPublic(env, lombokMethodAnnotationOnField); } - AnnotationMirror lombokMethodAnnotationOnElement = env.getAnnotation(getOwnerElement(), annotation); + AnnotationMirror lombokMethodAnnotationOnElement = env.getAnnotation(getDeclaringElement(), annotation); if (lombokMethodAnnotationOnElement != null) { return isAccessLevelPublic(env, lombokMethodAnnotationOnElement); } - return (env.hasAnnotation(getOwnerElement(), LOMBOK_DATA_ANNOTATION) - || env.hasAnnotation(getOwnerElement(), LOMBOK_VALUE_ANNOTATION)); + return (env.hasAnnotation(getDeclaringElement(), LOMBOK_DATA_ANNOTATION) + || env.hasAnnotation(getDeclaringElement(), LOMBOK_VALUE_ANNOTATION)); } private boolean isAccessLevelPublic(MetadataGenerationEnvironment env, AnnotationMirror lombokAnnotation) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java new file mode 100644 index 00000000000..806f5cc2d54 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java @@ -0,0 +1,216 @@ +/* + * Copyright 2012-2024 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.Map; +import java.util.function.Function; + +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; + +/** + * {@link PropertyDescriptor} created from a constructor or record parameter. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +abstract class ParameterPropertyDescriptor extends PropertyDescriptor { + + private final VariableElement parameter; + + ParameterPropertyDescriptor(String name, TypeMirror type, VariableElement parameter, TypeElement declaringElement, + ExecutableElement getter) { + super(name, type, declaringElement, getter); + this.parameter = parameter; + + } + + final VariableElement getParameter() { + return this.parameter; + } + + @Override + protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) { + Object defaultValue = getDefaultValueFromAnnotation(environment, getParameter()); + return (defaultValue != null) ? defaultValue + : getParameter().asType().accept(DefaultPrimitiveTypeVisitor.INSTANCE, null); + } + + private Object getDefaultValueFromAnnotation(MetadataGenerationEnvironment environment, Element element) { + AnnotationMirror annotation = environment.getDefaultValueAnnotation(element); + List defaultValue = getDefaultValue(environment, annotation); + if (defaultValue != null) { + TypeMirror specificType = determineSpecificType(environment); + try { + List coerced = defaultValue.stream().map((value) -> coerceValue(specificType, value)).toList(); + return (coerced.size() != 1) ? coerced : coerced.get(0); + } + catch (IllegalArgumentException ex) { + environment.getMessager().printMessage(Kind.ERROR, ex.getMessage(), element, annotation); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private List getDefaultValue(MetadataGenerationEnvironment environment, AnnotationMirror annotation) { + if (annotation == null) { + return null; + } + Map values = environment.getAnnotationElementValues(annotation); + return (List) values.get("value"); + } + + private TypeMirror determineSpecificType(MetadataGenerationEnvironment environment) { + TypeMirror parameterType = getParameter().asType(); + TypeMirror elementType = environment.getTypeUtils().extractElementType(parameterType); + parameterType = (elementType != null) ? elementType : parameterType; + PrimitiveType primitiveType = environment.getTypeUtils().getPrimitiveType(parameterType); + return (primitiveType != null) ? primitiveType : parameterType; + } + + private Object coerceValue(TypeMirror type, String value) { + Object coercedValue = type.accept(DefaultValueCoercionTypeVisitor.INSTANCE, value); + return (coercedValue != null) ? coercedValue : value; + } + + @Override + public boolean isProperty(MetadataGenerationEnvironment env) { + return !isNested(env); // We must be able to bind it to build the object. + } + + /** + * Visitor that gets the default value for primitives. + */ + private static final class DefaultPrimitiveTypeVisitor extends TypeKindVisitor8 { + + static final DefaultPrimitiveTypeVisitor INSTANCE = new DefaultPrimitiveTypeVisitor(); + + @Override + public Object visitPrimitiveAsBoolean(PrimitiveType type, Void parameter) { + return false; + } + + @Override + public Object visitPrimitiveAsByte(PrimitiveType type, Void parameter) { + return (byte) 0; + } + + @Override + public Object visitPrimitiveAsShort(PrimitiveType type, Void parameter) { + return (short) 0; + } + + @Override + public Object visitPrimitiveAsInt(PrimitiveType type, Void parameter) { + return 0; + } + + @Override + public Object visitPrimitiveAsLong(PrimitiveType type, Void parameter) { + return 0L; + } + + @Override + public Object visitPrimitiveAsChar(PrimitiveType type, Void parameter) { + return null; + } + + @Override + public Object visitPrimitiveAsFloat(PrimitiveType type, Void parameter) { + return 0F; + } + + @Override + public Object visitPrimitiveAsDouble(PrimitiveType type, Void parameter) { + return 0D; + } + + } + + /** + * Visitor that gets the default using coercion. + */ + private static final class DefaultValueCoercionTypeVisitor extends TypeKindVisitor8 { + + static final DefaultValueCoercionTypeVisitor INSTANCE = new DefaultValueCoercionTypeVisitor(); + + private T parseNumber(String value, Function parser, + PrimitiveType primitiveType) { + try { + return parser.apply(value); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException( + String.format("Invalid %s representation '%s'", primitiveType, value)); + } + } + + @Override + public Object visitPrimitiveAsBoolean(PrimitiveType type, String value) { + return Boolean.parseBoolean(value); + } + + @Override + public Object visitPrimitiveAsByte(PrimitiveType type, String value) { + return parseNumber(value, Byte::parseByte, type); + } + + @Override + public Object visitPrimitiveAsShort(PrimitiveType type, String value) { + return parseNumber(value, Short::parseShort, type); + } + + @Override + public Object visitPrimitiveAsInt(PrimitiveType type, String value) { + return parseNumber(value, Integer::parseInt, type); + } + + @Override + public Object visitPrimitiveAsLong(PrimitiveType type, String value) { + return parseNumber(value, Long::parseLong, type); + } + + @Override + public Object visitPrimitiveAsChar(PrimitiveType type, String value) { + if (value.length() > 1) { + throw new IllegalArgumentException(String.format("Invalid character representation '%s'", value)); + } + return value; + } + + @Override + public Object visitPrimitiveAsFloat(PrimitiveType type, String value) { + return parseNumber(value, Float::parseFloat, type); + } + + @Override + public Object visitPrimitiveAsDouble(PrimitiveType type, String value) { + return parseNumber(value, Double::parseDouble, type); + } + + } + +} 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 83cc3470422..386f5b2992c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 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. @@ -16,11 +16,12 @@ package org.springframework.boot.configurationprocessor; +import java.util.List; + import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; 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 org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; @@ -30,134 +31,107 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; /** * Description of a property that can be candidate for metadata generation. * - * @param the type of the source element that determines the property * @author Stephane Nicoll + * @author Phillip Webb */ -abstract class PropertyDescriptor { - - private final TypeElement ownerElement; - - private final ExecutableElement factoryMethod; - - private final S source; +abstract class PropertyDescriptor { private final String name; private final TypeMirror type; - private final VariableElement field; + private final TypeElement declaringElement; private final ExecutableElement getter; - private final ExecutableElement setter; - - protected PropertyDescriptor(TypeElement ownerElement, ExecutableElement factoryMethod, S source, String name, - TypeMirror type, VariableElement field, ExecutableElement getter, ExecutableElement setter) { - this.ownerElement = ownerElement; - this.factoryMethod = factoryMethod; - this.source = source; + /** + * Create a new {@link PropertyDescriptor} instance. + * @param name the property name + * @param type the property type + * @param declaringElement the element that declared the item + * @param getter the getter for the property or {@code null} + */ + PropertyDescriptor(String name, TypeMirror type, TypeElement declaringElement, ExecutableElement getter) { + this.declaringElement = declaringElement; this.name = name; this.type = type; - this.field = field; this.getter = getter; - this.setter = setter; - } - - TypeElement getOwnerElement() { - return this.ownerElement; - } - - ExecutableElement getFactoryMethod() { - return this.factoryMethod; - } - - S getSource() { - return this.source; } + /** + * Return the name of the property. + * @return the property name + */ String getName() { return this.name; } + /** + * Return the type of the property. + * @return the property type + */ TypeMirror getType() { return this.type; } - VariableElement getField() { - return this.field; + /** + * Return the element that declared the property. + * @return the declaring element + */ + protected final TypeElement getDeclaringElement() { + return this.declaringElement; } - ExecutableElement getGetter() { + /** + * Return the getter for the property. + * @return the getter or {@code null} + */ + protected final ExecutableElement getGetter() { return this.getter; } - ExecutableElement getSetter() { - return this.setter; - } - - protected abstract boolean isProperty(MetadataGenerationEnvironment environment); - - protected abstract Object resolveDefaultValue(MetadataGenerationEnvironment environment); - - protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) { - boolean deprecated = environment.isDeprecated(getGetter()) || environment.isDeprecated(getSetter()) - || environment.isDeprecated(getField()) || environment.isDeprecated(getFactoryMethod()); - return deprecated ? environment.resolveItemDeprecation(getGetter()) : null; - } - - protected boolean isNested(MetadataGenerationEnvironment environment) { - Element typeElement = environment.getTypeUtils().asElement(getType()); - if (!(typeElement instanceof TypeElement) || typeElement.getKind() == ElementKind.ENUM) { - return false; - } - if (environment.getConfigurationPropertiesAnnotation(getGetter()) != null) { - return false; - } - if (environment.getNestedConfigurationPropertyAnnotation(getField()) != null) { - return true; - } - if (isCyclePresent(typeElement, getOwnerElement())) { - return false; - } - return isParentTheSame(environment, typeElement, getOwnerElement()); - } - - ItemMetadata resolveItemMetadata(String prefix, MetadataGenerationEnvironment environment) { + /** + * Resolve the {@link ItemMetadata} for this property. + * @param prefix the property prefix + * @param environment the metadata generation environment + * @return the item metadata or {@code null} + */ + final ItemMetadata resolveItemMetadata(String prefix, MetadataGenerationEnvironment environment) { if (isNested(environment)) { return resolveItemMetadataGroup(prefix, environment); } - else if (isProperty(environment)) { + if (isProperty(environment)) { return resolveItemMetadataProperty(prefix, environment); } return null; } - private ItemMetadata resolveItemMetadataProperty(String prefix, MetadataGenerationEnvironment environment) { - String dataType = resolveType(environment); - String ownerType = environment.getTypeUtils().getQualifiedName(getOwnerElement()); - String description = resolveDescription(environment); - Object defaultValue = resolveDefaultValue(environment); - ItemDeprecation deprecation = resolveItemDeprecation(environment); - return ItemMetadata.newProperty(prefix, getName(), dataType, ownerType, null, description, defaultValue, - deprecation); - } - - private ItemMetadata resolveItemMetadataGroup(String prefix, MetadataGenerationEnvironment environment) { - Element propertyElement = environment.getTypeUtils().asElement(getType()); - String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, getName()); - String dataType = environment.getTypeUtils().getQualifiedName(propertyElement); - String ownerType = environment.getTypeUtils().getQualifiedName(getOwnerElement()); - String sourceMethod = (getGetter() != null) ? getGetter().toString() : null; - return ItemMetadata.newGroup(nestedPrefix, dataType, ownerType, sourceMethod); - } - - private String resolveType(MetadataGenerationEnvironment environment) { - return environment.getTypeUtils().getType(getOwnerElement(), getType()); + /** + * Return if this is a nested property. + * @param environment the metadata generation environment + * @return if the property is nested + * @see #isMarkedAsNested(MetadataGenerationEnvironment) + */ + boolean isNested(MetadataGenerationEnvironment environment) { + Element typeElement = environment.getTypeUtils().asElement(getType()); + if (!(typeElement instanceof TypeElement) || typeElement.getKind() == ElementKind.ENUM + || environment.getConfigurationPropertiesAnnotation(getGetter()) != null) { + return false; + } + if (isMarkedAsNested(environment)) { + return true; + } + return !isCyclePresent(typeElement, getDeclaringElement()) + && isParentTheSame(environment, typeElement, getDeclaringElement()); } - private String resolveDescription(MetadataGenerationEnvironment environment) { - return environment.getTypeUtils().getJavaDoc(getField()); - } + /** + * Return if this property has been explicitly marked as nested (for example using an + * annotation}. + * @param environment the metadata generation environment + * @return if the property has been marked as nested + */ + protected abstract boolean isMarkedAsNested(MetadataGenerationEnvironment environment); private boolean isCyclePresent(Element returnType, Element element) { if (!(element.getEnclosingElement() instanceof TypeElement)) { @@ -192,4 +166,60 @@ abstract class PropertyDescriptor { return getTopLevelType(element.getEnclosingElement()); } + private ItemMetadata resolveItemMetadataGroup(String prefix, MetadataGenerationEnvironment environment) { + Element propertyElement = environment.getTypeUtils().asElement(getType()); + String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, getName()); + String dataType = environment.getTypeUtils().getQualifiedName(propertyElement); + String ownerType = environment.getTypeUtils().getQualifiedName(getDeclaringElement()); + String sourceMethod = (getGetter() != null) ? getGetter().toString() : null; + return ItemMetadata.newGroup(nestedPrefix, dataType, ownerType, sourceMethod); + } + + private ItemMetadata resolveItemMetadataProperty(String prefix, MetadataGenerationEnvironment environment) { + String dataType = resolveType(environment); + String ownerType = environment.getTypeUtils().getQualifiedName(getDeclaringElement()); + String description = resolveDescription(environment); + Object defaultValue = resolveDefaultValue(environment); + ItemDeprecation deprecation = resolveItemDeprecation(environment); + return ItemMetadata.newProperty(prefix, getName(), dataType, ownerType, null, description, defaultValue, + deprecation); + } + + private String resolveType(MetadataGenerationEnvironment environment) { + return environment.getTypeUtils().getType(getDeclaringElement(), getType()); + } + + private ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) { + boolean deprecated = getDeprecatableElements().stream().anyMatch(environment::isDeprecated); + return deprecated ? environment.resolveItemDeprecation(getGetter()) : null; + } + + /** + * Resolve the property description. + * @param environment the metadata generation environment + * @return the property description + */ + protected abstract String resolveDescription(MetadataGenerationEnvironment environment); + + /** + * Resolve the default value for this property. + * @param environment the metadata generation environment + * @return the default value or {@code null} + */ + protected abstract Object resolveDefaultValue(MetadataGenerationEnvironment environment); + + /** + * Return all the elements that should be considered when checking for deprecation + * annotations. + * @return the deprecatable elements + */ + protected abstract List getDeprecatableElements(); + + /** + * Return true if this descriptor is for a property. + * @param environment the metadata generation environment + * @return if this is a property + */ + abstract boolean isProperty(MetadataGenerationEnvironment environment); + } 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 c4808a93a71..51fe3016ec2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.NestingKind; +import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; @@ -36,6 +37,7 @@ import javax.lang.model.util.ElementFilter; * * @author Stephane Nicoll * @author Phillip Webb + * @author Pavel Anisimov */ class PropertyDescriptorResolver { @@ -54,40 +56,48 @@ class PropertyDescriptorResolver { * or {@code null} * @return the candidate properties for metadata generation */ - Stream> resolve(TypeElement type, ExecutableElement factoryMethod) { + Stream resolve(TypeElement type, ExecutableElement factoryMethod) { TypeElementMembers members = new TypeElementMembers(this.environment, type); if (factoryMethod != null) { - return resolveJavaBeanProperties(type, factoryMethod, members); + return resolveJavaBeanProperties(type, members, factoryMethod); } - return resolve(ConfigurationPropertiesTypeElement.of(type, this.environment), members); + return resolve(Bindable.of(type, this.environment), members); } - private Stream> resolve(ConfigurationPropertiesTypeElement type, TypeElementMembers members) { - if (type.isConstructorBindingEnabled()) { - ExecutableElement constructor = type.getBindConstructor(); - if (constructor != null) { - return resolveConstructorProperties(type.getType(), members, constructor); - } - return Stream.empty(); + private Stream resolve(Bindable bindable, TypeElementMembers members) { + if (bindable.isConstructorBindingEnabled()) { + ExecutableElement bindConstructor = bindable.getBindConstructor(); + return (bindConstructor != null) + ? resolveConstructorBoundProperties(bindable.getType(), members, bindConstructor) : Stream.empty(); } - return resolveJavaBeanProperties(type.getType(), null, members); + return resolveJavaBeanProperties(bindable.getType(), members, null); } - Stream> resolveConstructorProperties(TypeElement type, TypeElementMembers members, - ExecutableElement constructor) { - Map> candidates = new LinkedHashMap<>(); - constructor.getParameters().forEach((parameter) -> { - String name = getParameterName(parameter); - 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, null, parameter, name, propertyType, - field, getter, setter)); + private Stream resolveConstructorBoundProperties(TypeElement declaringElement, + TypeElementMembers members, ExecutableElement bindConstructor) { + Map candidates = new LinkedHashMap<>(); + bindConstructor.getParameters().forEach((parameter) -> { + PropertyDescriptor descriptor = extracted(declaringElement, members, parameter); + register(candidates, descriptor); }); return candidates.values().stream(); } + private PropertyDescriptor extracted(TypeElement declaringElement, TypeElementMembers members, + VariableElement parameter) { + String name = getParameterName(parameter); + TypeMirror type = parameter.asType(); + ExecutableElement getter = members.getPublicGetter(name, type); + ExecutableElement setter = members.getPublicSetter(name, type); + VariableElement field = members.getFields().get(name); + RecordComponentElement recordComponent = members.getRecordComponents().get(name); + return (recordComponent != null) + ? new RecordParameterPropertyDescriptor(name, type, parameter, declaringElement, getter, + recordComponent) + : new ConstructorParameterPropertyDescriptor(name, type, parameter, declaringElement, getter, setter, + field); + } + private String getParameterName(VariableElement parameter) { AnnotationMirror nameAnnotation = this.environment.getNameAnnotation(parameter); if (nameAnnotation != null) { @@ -96,24 +106,24 @@ class PropertyDescriptorResolver { return parameter.getSimpleName().toString(); } - Stream> resolveJavaBeanProperties(TypeElement type, ExecutableElement factoryMethod, - TypeElementMembers members) { + private Stream resolveJavaBeanProperties(TypeElement declaringElement, + TypeElementMembers members, ExecutableElement factoryMethod) { // First check if we have regular java bean properties there - Map> candidates = new LinkedHashMap<>(); + Map candidates = new LinkedHashMap<>(); members.getPublicGetters().forEach((name, getters) -> { VariableElement field = members.getFields().get(name); ExecutableElement getter = findMatchingGetter(members, getters, field); TypeMirror propertyType = getter.getReturnType(); - register(candidates, new JavaBeanPropertyDescriptor(type, factoryMethod, getter, name, propertyType, field, - members.getPublicSetter(name, propertyType))); + register(candidates, new JavaBeanPropertyDescriptor(name, propertyType, declaringElement, getter, + members.getPublicSetter(name, propertyType), field, factoryMethod)); }); // Then check for Lombok ones members.getFields().forEach((name, field) -> { 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)); + register(candidates, new LombokPropertyDescriptor(name, propertyType, declaringElement, getter, setter, + field, factoryMethod)); }); return candidates.values().stream(); } @@ -126,20 +136,20 @@ class PropertyDescriptorResolver { return candidates.get(0); } - private void register(Map> candidates, PropertyDescriptor descriptor) { + private void register(Map candidates, PropertyDescriptor descriptor) { if (!candidates.containsKey(descriptor.getName()) && isCandidate(descriptor)) { candidates.put(descriptor.getName(), descriptor); } } - private boolean isCandidate(PropertyDescriptor descriptor) { + private boolean isCandidate(PropertyDescriptor descriptor) { return descriptor.isProperty(this.environment) || descriptor.isNested(this.environment); } /** * Wrapper around a {@link TypeElement} that could be bound. */ - private static class ConfigurationPropertiesTypeElement { + private static class Bindable { private final TypeElement type; @@ -147,8 +157,7 @@ class PropertyDescriptorResolver { private final List boundConstructors; - ConfigurationPropertiesTypeElement(TypeElement type, List constructors, - List boundConstructors) { + Bindable(TypeElement type, List constructors, List boundConstructors) { this.type = type; this.constructors = constructors; this.boundConstructors = boundConstructors; @@ -185,10 +194,10 @@ class PropertyDescriptorResolver { return boundConstructor; } - static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) { + static Bindable of(TypeElement type, MetadataGenerationEnvironment env) { List constructors = ElementFilter.constructorsIn(type.getEnclosedElements()); List boundConstructors = getBoundConstructors(type, env, constructors); - return new ConfigurationPropertiesTypeElement(type, constructors, boundConstructors); + return new Bindable(type, constructors, boundConstructors); } private static List getBoundConstructors(TypeElement type, MetadataGenerationEnvironment env, diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java new file mode 100644 index 00000000000..f9ea960b0ad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2024 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.Arrays; +import java.util.List; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * A {@link PropertyDescriptor} for a record parameter. + * + * @author Stephane Nicoll + * @author Pavel Anisimov + * @author Phillip Webb + */ +class RecordParameterPropertyDescriptor extends ParameterPropertyDescriptor { + + private final RecordComponentElement recordComponent; + + RecordParameterPropertyDescriptor(String name, TypeMirror type, VariableElement parameter, + TypeElement declaringElement, ExecutableElement getter, RecordComponentElement recordComponent) { + super(name, type, parameter, declaringElement, getter); + this.recordComponent = recordComponent; + } + + @Override + protected List getDeprecatableElements() { + return Arrays.asList(getGetter()); + } + + @Override + protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) { + return false; + } + + @Override + protected String resolveDescription(MetadataGenerationEnvironment environment) { + return environment.getTypeUtils().getJavaDoc(this.recordComponent); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java index d7b073e7110..5aa3b921d60 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -27,6 +27,7 @@ import java.util.function.Function; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; @@ -39,12 +40,13 @@ import javax.lang.model.util.ElementFilter; * @author Stephane Nicoll * @author Phillip Webb * @author Moritz Halbritter + * @author Pavel Anisimov */ class TypeElementMembers { private static final String OBJECT_CLASS_NAME = Object.class.getName(); - private static final String RECORD_CLASS_NAME = "java.lang.Record"; + private static final String RECORD_CLASS_NAME = Record.class.getName(); private final MetadataGenerationEnvironment env; @@ -54,6 +56,8 @@ class TypeElementMembers { private final Map fields = new LinkedHashMap<>(); + private final Map recordComponents = new LinkedHashMap<>(); + private final Map> publicGetters = new LinkedHashMap<>(); private final Map> publicSetters = new LinkedHashMap<>(); @@ -69,6 +73,9 @@ class TypeElementMembers { for (VariableElement field : ElementFilter.fieldsIn(element.getEnclosedElements())) { processField(field); } + for (RecordComponentElement recordComponent : ElementFilter.recordComponentsIn(element.getEnclosedElements())) { + processRecordComponent(recordComponent); + } for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) { processMethod(method); } @@ -189,10 +196,19 @@ class TypeElementMembers { this.fields.putIfAbsent(name, field); } + private void processRecordComponent(RecordComponentElement recordComponent) { + String name = recordComponent.getSimpleName().toString(); + this.recordComponents.putIfAbsent(name, recordComponent); + } + Map getFields() { return Collections.unmodifiableMap(this.fields); } + Map getRecordComponents() { + return Collections.unmodifiableMap(this.recordComponents); + } + Map> getPublicGetters() { return Collections.unmodifiableMap(this.publicGetters); } 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 7f94f1667ba..568b9e6e249 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 @@ -24,11 +24,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; +import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; @@ -44,6 +46,7 @@ import javax.lang.model.util.Types; * * @author Stephane Nicoll * @author Phillip Webb + * @author Pavel Anisimov */ class TypeUtils { @@ -176,6 +179,9 @@ class TypeUtils { } String getJavaDoc(Element element) { + if (element instanceof RecordComponentElement) { + return getJavaDoc((RecordComponentElement) element); + } String javadoc = (element != null) ? this.env.getElementUtils().getDocComment(element) : null; if (javadoc != null) { javadoc = NEW_LINE_PATTERN.matcher(javadoc).replaceAll("").trim(); @@ -247,6 +253,24 @@ class TypeUtils { } } + private String getJavaDoc(RecordComponentElement recordComponent) { + String recordJavadoc = this.env.getElementUtils().getDocComment(recordComponent.getEnclosingElement()); + if (recordJavadoc != null) { + Pattern paramJavadocPattern = paramJavadocPattern(recordComponent.getSimpleName().toString()); + Matcher paramJavadocMatcher = paramJavadocPattern.matcher(recordJavadoc); + if (paramJavadocMatcher.find()) { + String paramJavadoc = NEW_LINE_PATTERN.matcher(paramJavadocMatcher.group()).replaceAll("").trim(); + return paramJavadoc.isEmpty() ? null : paramJavadoc; + } + } + return null; + } + + private Pattern paramJavadocPattern(String paramName) { + String pattern = String.format("(?<=@param +%s).*?(?=([\r\n]+ *@)|$)", paramName); + return Pattern.compile(pattern, Pattern.DOTALL); + } + /** * A visitor that extracts the fully qualified name of a type, including generic * information. 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 bc58efcde3a..8aaa49d316b 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 @@ -22,6 +22,7 @@ import org.springframework.boot.configurationprocessor.metadata.ConfigurationMet import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.deprecation.Dbcp2Configuration; +import org.springframework.boot.configurationsample.record.ExampleRecord; import org.springframework.boot.configurationsample.record.RecordWithGetter; import org.springframework.boot.configurationsample.recursive.RecursiveProperties; import org.springframework.boot.configurationsample.simple.ClassWithNestedProperties; @@ -516,4 +517,19 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene assertThat(metadata).has(Metadata.withProperty("spring.datasource.dbcp2.password").withNoDeprecation()); } + @Test + void recordPropertiesWithDescriptions() { + ConfigurationMetadata metadata = compile(ExampleRecord.class); + assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-string", String.class) + .withDescription("very long description that doesn't fit single line")); + assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-integer", Integer.class) + .withDescription("description with @param and @ pitfalls")); + assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-boolean", Boolean.class) + .withDescription("description with extra spaces")); + assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-long", Long.class) + .withDescription("description without space after asterisk")); + assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-byte", Byte.class) + .withDescription("last description in Javadoc")); + } + } 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 index 01284252bad..4faed490cd0 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -48,7 +48,7 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest TypeElement ownerElement = roundEnv.getRootElement(ImmutableSimpleProperties.class); ConstructorParameterPropertyDescriptor property = createPropertyDescriptor(ownerElement, "theName"); assertThat(property.getName()).isEqualTo("theName"); - assertThat(property.getSource()).hasToString("theName"); + assertThat(property.getParameter()).hasToString("theName"); assertThat(property.getGetter().getSimpleName()).hasToString("getTheName"); assertThat(property.isProperty(metadataEnv)).isTrue(); assertThat(property.isNested(metadataEnv)).isFalse(); @@ -61,7 +61,7 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest TypeElement ownerElement = roundEnv.getRootElement(ImmutableInnerClassProperties.class); ConstructorParameterPropertyDescriptor property = createPropertyDescriptor(ownerElement, "first"); assertThat(property.getName()).isEqualTo("first"); - assertThat(property.getSource()).hasToString("first"); + assertThat(property.getParameter()).hasToString("first"); assertThat(property.getGetter().getSimpleName()).hasToString("getFirst"); assertThat(property.isProperty(metadataEnv)).isFalse(); assertThat(property.isNested(metadataEnv)).isTrue(); @@ -74,7 +74,7 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest TypeElement ownerElement = roundEnv.getRootElement(ImmutableInnerClassProperties.class); ConstructorParameterPropertyDescriptor property = createPropertyDescriptor(ownerElement, "third"); assertThat(property.getName()).isEqualTo("third"); - assertThat(property.getSource()).hasToString("third"); + assertThat(property.getParameter()).hasToString("third"); assertThat(property.getGetter().getSimpleName()).hasToString("getThird"); assertThat(property.isProperty(metadataEnv)).isFalse(); assertThat(property.isNested(metadataEnv)).isTrue(); @@ -87,7 +87,7 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest TypeElement ownerElement = roundEnv.getRootElement(ImmutableSimpleProperties.class); ConstructorParameterPropertyDescriptor property = createPropertyDescriptor(ownerElement, "counter"); assertThat(property.getName()).isEqualTo("counter"); - assertThat(property.getSource()).hasToString("counter"); + assertThat(property.getParameter()).hasToString("counter"); assertThat(property.getGetter()).isNull(); assertThat(property.isProperty(metadataEnv)).isTrue(); assertThat(property.isNested(metadataEnv)).isFalse(); @@ -130,8 +130,8 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest 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); + ConstructorParameterPropertyDescriptor property = new ConstructorParameterPropertyDescriptor("flag", + field.asType(), constructorParameter, ownerElement, getter, null, field); assertItemMetadata(metadataEnv, property).isProperty().isDeprecatedWithNoInformation(); }); } @@ -222,8 +222,8 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest 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); + return new ConstructorParameterPropertyDescriptor(name, field.asType(), constructorParameter, ownerElement, + getter, setter, field); } private VariableElement getConstructorParameter(TypeElement ownerElement, String name) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java index d763c13ca42..5c1f2b74f03 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -43,7 +43,6 @@ class JavaBeanPropertyDescriptorTests extends PropertyDescriptorTests { TypeElement ownerElement = roundEnv.getRootElement(SimpleTypeProperties.class); JavaBeanPropertyDescriptor property = createPropertyDescriptor(ownerElement, "myString"); assertThat(property.getName()).isEqualTo("myString"); - assertThat(property.getSource()).isSameAs(property.getGetter()); assertThat(property.getGetter().getSimpleName()).hasToString("getMyString"); assertThat(property.getSetter().getSimpleName()).hasToString("setMyString"); assertThat(property.isProperty(metadataEnv)).isTrue(); @@ -96,10 +95,9 @@ class JavaBeanPropertyDescriptorTests extends PropertyDescriptorTests { TypeElement ownerElement = roundEnv.getRootElement(SimpleProperties.class); ExecutableElement getter = getMethod(ownerElement, "getSize"); VariableElement field = getField(ownerElement, "size"); - JavaBeanPropertyDescriptor property = new JavaBeanPropertyDescriptor(ownerElement, getter, getter, "size", - field.asType(), field, null); + JavaBeanPropertyDescriptor property = new JavaBeanPropertyDescriptor("size", field.asType(), ownerElement, + getter, null, field, getter); assertThat(property.getName()).isEqualTo("size"); - assertThat(property.getSource()).isSameAs(property.getGetter()); assertThat(property.getGetter().getSimpleName()).hasToString("getSize"); assertThat(property.getSetter()).isNull(); assertThat(property.isProperty(metadataEnv)).isFalse(); @@ -112,10 +110,9 @@ class JavaBeanPropertyDescriptorTests extends PropertyDescriptorTests { process(SimpleProperties.class, (roundEnv, metadataEnv) -> { TypeElement ownerElement = roundEnv.getRootElement(SimpleProperties.class); VariableElement field = getField(ownerElement, "counter"); - JavaBeanPropertyDescriptor property = new JavaBeanPropertyDescriptor(ownerElement, null, null, "counter", - field.asType(), field, getMethod(ownerElement, "setCounter")); + JavaBeanPropertyDescriptor property = new JavaBeanPropertyDescriptor("counter", field.asType(), + ownerElement, null, getMethod(ownerElement, "setCounter"), field, null); assertThat(property.getName()).isEqualTo("counter"); - assertThat(property.getSource()).isSameAs(property.getGetter()); assertThat(property.getGetter()).isNull(); assertThat(property.getSetter().getSimpleName()).hasToString("setCounter"); assertThat(property.isProperty(metadataEnv)).isFalse(); @@ -171,8 +168,8 @@ class JavaBeanPropertyDescriptorTests extends PropertyDescriptorTests { process(SimpleProperties.class, (roundEnv, metadataEnv) -> { TypeElement ownerElement = roundEnv.getRootElement(SimpleProperties.class); VariableElement field = getField(ownerElement, "counter"); - JavaBeanPropertyDescriptor property = new JavaBeanPropertyDescriptor(ownerElement, null, null, "counter", - field.asType(), field, getMethod(ownerElement, "setCounter")); + JavaBeanPropertyDescriptor property = new JavaBeanPropertyDescriptor("counter", field.asType(), + ownerElement, null, getMethod(ownerElement, "setCounter"), field, null); assertThat(property.resolveItemMetadata("test", metadataEnv)).isNull(); }); } @@ -247,7 +244,7 @@ class JavaBeanPropertyDescriptorTests extends PropertyDescriptorTests { ExecutableElement getter = getMethod(ownerElement, getterName); ExecutableElement setter = getMethod(ownerElement, setterName); VariableElement field = getField(ownerElement, name); - return new JavaBeanPropertyDescriptor(ownerElement, null, getter, name, getter.getReturnType(), field, setter); + return new JavaBeanPropertyDescriptor(name, getter.getReturnType(), ownerElement, getter, setter, field, null); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java index 99f260cdf1d..ccde69102bd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -47,7 +47,6 @@ class LombokPropertyDescriptorTests extends PropertyDescriptorTests { TypeElement ownerElement = roundEnv.getRootElement(LombokSimpleProperties.class); LombokPropertyDescriptor property = createPropertyDescriptor(ownerElement, "name"); assertThat(property.getName()).isEqualTo("name"); - assertThat(property.getSource()).isSameAs(property.getField()); assertThat(property.getField().getSimpleName()).hasToString("name"); assertThat(property.isProperty(metadataEnv)).isTrue(); assertThat(property.isNested(metadataEnv)).isFalse(); @@ -60,7 +59,6 @@ class LombokPropertyDescriptorTests extends PropertyDescriptorTests { TypeElement ownerElement = roundEnv.getRootElement(LombokSimpleProperties.class); LombokPropertyDescriptor property = createPropertyDescriptor(ownerElement, "items"); assertThat(property.getName()).isEqualTo("items"); - assertThat(property.getSource()).isSameAs(property.getField()); assertThat(property.getField().getSimpleName()).hasToString("items"); assertThat(property.isProperty(metadataEnv)).isTrue(); assertThat(property.isNested(metadataEnv)).isFalse(); @@ -73,7 +71,6 @@ class LombokPropertyDescriptorTests extends PropertyDescriptorTests { TypeElement ownerElement = roundEnv.getRootElement(LombokInnerClassProperties.class); LombokPropertyDescriptor property = createPropertyDescriptor(ownerElement, "first"); assertThat(property.getName()).isEqualTo("first"); - assertThat(property.getSource()).isSameAs(property.getField()); assertThat(property.getField().getSimpleName()).hasToString("first"); assertThat(property.isProperty(metadataEnv)).isFalse(); assertThat(property.isNested(metadataEnv)).isTrue(); @@ -86,7 +83,6 @@ class LombokPropertyDescriptorTests extends PropertyDescriptorTests { TypeElement ownerElement = roundEnv.getRootElement(LombokInnerClassProperties.class); LombokPropertyDescriptor property = createPropertyDescriptor(ownerElement, "third"); assertThat(property.getName()).isEqualTo("third"); - assertThat(property.getSource()).isSameAs(property.getField()); assertThat(property.getField().getSimpleName()).hasToString("third"); assertThat(property.isProperty(metadataEnv)).isFalse(); assertThat(property.isNested(metadataEnv)).isTrue(); @@ -177,8 +173,8 @@ class LombokPropertyDescriptorTests extends PropertyDescriptorTests { TypeElement ownerElement = roundEnv.getRootElement(LombokInnerClassProperties.class); VariableElement field = getField(ownerElement, "third"); ExecutableElement getter = getMethod(ownerElement, "getThird"); - LombokPropertyDescriptor property = new LombokPropertyDescriptor(ownerElement, null, field, "third", - field.asType(), getter, null); + LombokPropertyDescriptor property = new LombokPropertyDescriptor("third", field.asType(), ownerElement, + getter, null, field, null); assertItemMetadata(metadataEnv, property).isGroup() .hasName("test.third") .hasType("org.springframework.boot.configurationsample.lombok.SimpleLombokPojo") @@ -276,7 +272,7 @@ class LombokPropertyDescriptorTests extends PropertyDescriptorTests { VariableElement field = getField(ownerElement, name); ExecutableElement getter = getMethod(ownerElement, createAccessorMethodName("get", name)); ExecutableElement setter = getMethod(ownerElement, createAccessorMethodName("set", name)); - return new LombokPropertyDescriptor(ownerElement, null, field, name, field.asType(), getter, setter); + return new LombokPropertyDescriptor(name, field.asType(), ownerElement, getter, setter, field, null); } } 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 9a2b1aeed87..686cdbb2f5b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -161,7 +161,7 @@ class PropertyDescriptorResolverTests { } private BiConsumer properties( - Consumer>> stream) { + Consumer> stream) { return (element, metadataEnv) -> { PropertyDescriptorResolver resolver = new PropertyDescriptorResolver(metadataEnv); stream.accept(resolver.resolve(element, null)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorTests.java index c2d5b8fac61..92ba92aeb6c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -61,7 +61,7 @@ public abstract class PropertyDescriptorTests { } protected ItemMetadataAssert assertItemMetadata(MetadataGenerationEnvironment metadataEnv, - PropertyDescriptor property) { + PropertyDescriptor property) { return new ItemMetadataAssert(property.resolveItemMetadata("test", metadataEnv)); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/record/ExampleRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/record/ExampleRecord.java new file mode 100644 index 00000000000..fdffebfbcbf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/record/ExampleRecord.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2024 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.record; + +/** + * Example Record Javadoc sample + * + * @param someString very long description that doesn't fit single line + * @param someInteger description with @param and @ pitfalls + * @param someBoolean description with extra spaces + * @param someLong description without space after asterisk + * @param someByte last description in Javadoc + * @since 1.0.0 + * @author Pavel Anisimov + */ +@org.springframework.boot.configurationsample.ConfigurationProperties("record.descriptions") +public record ExampleRecord(String someString, Integer someInteger, Boolean someBoolean, Long someLong, Byte someByte) { +}