Browse Source

Merge pull request #29403 from An1s9n

* pr/29403:
  Polish 'Generate configuration metadata for records'
  Generate configuration metadata for records

Closes gh-29403
pull/40477/head
Phillip Webb 2 years ago
parent
commit
e8263c8a8f
  1. 2
      spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc
  2. 2
      spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc
  3. 186
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java
  4. 53
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java
  5. 70
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java
  6. 216
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java
  7. 214
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java
  8. 83
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java
  9. 61
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java
  10. 20
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java
  11. 24
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java
  12. 16
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java
  13. 18
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java
  14. 19
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java
  15. 12
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java
  16. 4
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java
  17. 4
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorTests.java
  18. 32
      spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/record/ExampleRecord.java

2
spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc

@ -255,6 +255,8 @@ include-code::AcmeProperties[] @@ -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".

2
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 @@ -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.

186
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java

@ -16,195 +16,47 @@ @@ -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<VariableElement> {
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<String> 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<String> getDefaultValue(MetadataGenerationEnvironment environment, AnnotationMirror annotation) {
if (annotation == null) {
return null;
}
Map<String, Object> values = environment.getAnnotationElementValues(annotation);
return (List<String>) 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<Element> getDeprecatableElements() {
return Arrays.asList(getGetter(), this.setter, this.field);
}
private static final class DefaultValueCoercionTypeVisitor extends TypeKindVisitor8<Object, String> {
private static final DefaultValueCoercionTypeVisitor INSTANCE = new DefaultValueCoercionTypeVisitor();
private <T extends Number> T parseNumber(String value, Function<String, T> 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<Object, Void> {
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);
}
}

53
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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<ExecutableElement> {
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<Element> 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);
}
}

70
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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<VariableElement> {
class LombokPropertyDescriptor extends PropertyDescriptor {
private static final String LOMBOK_DATA_ANNOTATION = "lombok.Data";
@ -44,44 +46,62 @@ class LombokPropertyDescriptor extends PropertyDescriptor<VariableElement> { @@ -44,44 +46,62 @@ class LombokPropertyDescriptor extends PropertyDescriptor<VariableElement> {
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<Element> 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<VariableElement> { @@ -98,12 +118,12 @@ class LombokPropertyDescriptor extends PropertyDescriptor<VariableElement> {
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) {

216
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java

@ -0,0 +1,216 @@ @@ -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<String> defaultValue = getDefaultValue(environment, annotation);
if (defaultValue != null) {
TypeMirror specificType = determineSpecificType(environment);
try {
List<Object> 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<String> getDefaultValue(MetadataGenerationEnvironment environment, AnnotationMirror annotation) {
if (annotation == null) {
return null;
}
Map<String, Object> values = environment.getAnnotationElementValues(annotation);
return (List<String>) 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<Object, Void> {
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<Object, String> {
static final DefaultValueCoercionTypeVisitor INSTANCE = new DefaultValueCoercionTypeVisitor();
private <T extends Number> T parseNumber(String value, Function<String, T> 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);
}
}
}

214
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -30,134 +31,107 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
/**
* Description of a property that can be candidate for metadata generation.
*
* @param <S> the type of the source element that determines the property
* @author Stephane Nicoll
* @author Phillip Webb
*/
abstract class PropertyDescriptor<S extends Element> {
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<S extends Element> { @@ -192,4 +166,60 @@ abstract class PropertyDescriptor<S extends Element> {
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<Element> 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);
}

83
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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 { @@ -54,40 +56,48 @@ class PropertyDescriptorResolver {
* or {@code null}
* @return the candidate properties for metadata generation
*/
Stream<PropertyDescriptor<?>> resolve(TypeElement type, ExecutableElement factoryMethod) {
Stream<PropertyDescriptor> 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<PropertyDescriptor<?>> 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<PropertyDescriptor> 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<PropertyDescriptor<?>> resolveConstructorProperties(TypeElement type, TypeElementMembers members,
ExecutableElement constructor) {
Map<String, PropertyDescriptor<?>> 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<PropertyDescriptor> resolveConstructorBoundProperties(TypeElement declaringElement,
TypeElementMembers members, ExecutableElement bindConstructor) {
Map<String, PropertyDescriptor> 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 { @@ -96,24 +106,24 @@ class PropertyDescriptorResolver {
return parameter.getSimpleName().toString();
}
Stream<PropertyDescriptor<?>> resolveJavaBeanProperties(TypeElement type, ExecutableElement factoryMethod,
TypeElementMembers members) {
private Stream<PropertyDescriptor> resolveJavaBeanProperties(TypeElement declaringElement,
TypeElementMembers members, ExecutableElement factoryMethod) {
// First check if we have regular java bean properties there
Map<String, PropertyDescriptor<?>> candidates = new LinkedHashMap<>();
Map<String, PropertyDescriptor> 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 { @@ -126,20 +136,20 @@ class PropertyDescriptorResolver {
return candidates.get(0);
}
private void register(Map<String, PropertyDescriptor<?>> candidates, PropertyDescriptor<?> descriptor) {
private void register(Map<String, PropertyDescriptor> 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 { @@ -147,8 +157,7 @@ class PropertyDescriptorResolver {
private final List<ExecutableElement> boundConstructors;
ConfigurationPropertiesTypeElement(TypeElement type, List<ExecutableElement> constructors,
List<ExecutableElement> boundConstructors) {
Bindable(TypeElement type, List<ExecutableElement> constructors, List<ExecutableElement> boundConstructors) {
this.type = type;
this.constructors = constructors;
this.boundConstructors = boundConstructors;
@ -185,10 +194,10 @@ class PropertyDescriptorResolver { @@ -185,10 +194,10 @@ class PropertyDescriptorResolver {
return boundConstructor;
}
static ConfigurationPropertiesTypeElement of(TypeElement type, MetadataGenerationEnvironment env) {
static Bindable of(TypeElement type, MetadataGenerationEnvironment env) {
List<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements());
List<ExecutableElement> boundConstructors = getBoundConstructors(type, env, constructors);
return new ConfigurationPropertiesTypeElement(type, constructors, boundConstructors);
return new Bindable(type, constructors, boundConstructors);
}
private static List<ExecutableElement> getBoundConstructors(TypeElement type, MetadataGenerationEnvironment env,

61
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java

@ -0,0 +1,61 @@ @@ -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<Element> 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);
}
}

20
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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 { @@ -54,6 +56,8 @@ class TypeElementMembers {
private final Map<String, VariableElement> fields = new LinkedHashMap<>();
private final Map<String, RecordComponentElement> recordComponents = new LinkedHashMap<>();
private final Map<String, List<ExecutableElement>> publicGetters = new LinkedHashMap<>();
private final Map<String, List<ExecutableElement>> publicSetters = new LinkedHashMap<>();
@ -69,6 +73,9 @@ class TypeElementMembers { @@ -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 { @@ -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<String, VariableElement> getFields() {
return Collections.unmodifiableMap(this.fields);
}
Map<String, RecordComponentElement> getRecordComponents() {
return Collections.unmodifiableMap(this.recordComponents);
}
Map<String, List<ExecutableElement>> getPublicGetters() {
return Collections.unmodifiableMap(this.publicGetters);
}

24
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; @@ -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; @@ -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 { @@ -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 { @@ -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.

16
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 @@ -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 @@ -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"));
}
}

18
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java

@ -1,5 +1,5 @@ @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) {

19
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}

12
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}

4
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -161,7 +161,7 @@ class PropertyDescriptorResolverTests {
}
private BiConsumer<TypeElement, MetadataGenerationEnvironment> properties(
Consumer<Stream<PropertyDescriptor<?>>> stream) {
Consumer<Stream<PropertyDescriptor>> stream) {
return (element, metadataEnv) -> {
PropertyDescriptorResolver resolver = new PropertyDescriptorResolver(metadataEnv);
stream.accept(resolver.resolve(element, null));

4
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -61,7 +61,7 @@ public abstract class PropertyDescriptorTests {
}
protected ItemMetadataAssert assertItemMetadata(MetadataGenerationEnvironment metadataEnv,
PropertyDescriptor<?> property) {
PropertyDescriptor property) {
return new ItemMetadataAssert(property.resolveItemMetadata("test", metadataEnv));
}

32
spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/record/ExampleRecord.java

@ -0,0 +1,32 @@ @@ -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) {
}
Loading…
Cancel
Save