Skip to content

Commit e8263c8

Browse files
committed
Merge pull request #29403 from An1s9n
* pr/29403: Polish 'Generate configuration metadata for records' Generate configuration metadata for records Closes gh-29403
2 parents cde9166 + bce468f commit e8263c8

File tree

18 files changed

+672
-364
lines changed

18 files changed

+672
-364
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ include-code::AcmeProperties[]
255255

256256
NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON.
257257

258+
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).
259+
258260
Here are some rules we follow internally to make sure descriptions are consistent:
259261

260262
* Do not start the description by "The" or "A".

spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ The Javadoc on fields is used to populate the `description` attribute. For insta
8989

9090
NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON.
9191

92+
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).
93+
9294
The annotation processor applies a number of heuristics to extract the default value from the source model.
9395
Default values have to be provided statically. In particular, do not refer to a constant defined in another class.
9496
Also, the annotation processor cannot auto-detect default values for ``Enum``s and ``Collections``s.

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

Lines changed: 19 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -16,195 +16,47 @@
1616

1717
package org.springframework.boot.configurationprocessor;
1818

19+
import java.util.Arrays;
1920
import java.util.List;
20-
import java.util.Map;
21-
import java.util.function.Function;
2221

23-
import javax.lang.model.element.AnnotationMirror;
2422
import javax.lang.model.element.Element;
2523
import javax.lang.model.element.ExecutableElement;
2624
import javax.lang.model.element.TypeElement;
2725
import javax.lang.model.element.VariableElement;
28-
import javax.lang.model.type.PrimitiveType;
2926
import javax.lang.model.type.TypeMirror;
30-
import javax.lang.model.util.TypeKindVisitor8;
31-
import javax.tools.Diagnostic.Kind;
3227

3328
/**
3429
* A {@link PropertyDescriptor} for a constructor parameter.
3530
*
3631
* @author Stephane Nicoll
32+
* @author Phillip Webb
3733
*/
38-
class ConstructorParameterPropertyDescriptor extends PropertyDescriptor<VariableElement> {
34+
class ConstructorParameterPropertyDescriptor extends ParameterPropertyDescriptor {
3935

40-
ConstructorParameterPropertyDescriptor(TypeElement ownerElement, ExecutableElement factoryMethod,
41-
VariableElement source, String name, TypeMirror type, VariableElement field, ExecutableElement getter,
42-
ExecutableElement setter) {
43-
super(ownerElement, factoryMethod, source, name, type, field, getter, setter);
44-
}
36+
private final ExecutableElement setter;
4537

46-
@Override
47-
protected boolean isProperty(MetadataGenerationEnvironment env) {
48-
// If it's a constructor parameter, it doesn't matter as we must be able to bind
49-
// it to build the object.
50-
return !isNested(env);
51-
}
38+
private final VariableElement field;
5239

53-
@Override
54-
protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) {
55-
Object defaultValue = getDefaultValueFromAnnotation(environment, getSource());
56-
if (defaultValue != null) {
57-
return defaultValue;
58-
}
59-
return getSource().asType().accept(DefaultPrimitiveTypeVisitor.INSTANCE, null);
40+
ConstructorParameterPropertyDescriptor(String name, TypeMirror type, VariableElement parameter,
41+
TypeElement declaringElement, ExecutableElement getter, ExecutableElement setter, VariableElement field) {
42+
super(name, type, parameter, declaringElement, getter);
43+
this.setter = setter;
44+
this.field = field;
6045
}
6146

62-
private Object getDefaultValueFromAnnotation(MetadataGenerationEnvironment environment, Element element) {
63-
AnnotationMirror annotation = environment.getDefaultValueAnnotation(element);
64-
List<String> defaultValue = getDefaultValue(environment, annotation);
65-
if (defaultValue != null) {
66-
try {
67-
TypeMirror specificType = determineSpecificType(environment);
68-
if (defaultValue.size() == 1) {
69-
return coerceValue(specificType, defaultValue.get(0));
70-
}
71-
return defaultValue.stream().map((value) -> coerceValue(specificType, value)).toList();
72-
}
73-
catch (IllegalArgumentException ex) {
74-
environment.getMessager().printMessage(Kind.ERROR, ex.getMessage(), element, annotation);
75-
}
76-
}
77-
return null;
78-
}
79-
80-
@SuppressWarnings("unchecked")
81-
private List<String> getDefaultValue(MetadataGenerationEnvironment environment, AnnotationMirror annotation) {
82-
if (annotation == null) {
83-
return null;
84-
}
85-
Map<String, Object> values = environment.getAnnotationElementValues(annotation);
86-
return (List<String>) values.get("value");
87-
}
88-
89-
private TypeMirror determineSpecificType(MetadataGenerationEnvironment environment) {
90-
TypeMirror candidate = getSource().asType();
91-
TypeMirror elementCandidate = environment.getTypeUtils().extractElementType(candidate);
92-
if (elementCandidate != null) {
93-
candidate = elementCandidate;
94-
}
95-
PrimitiveType primitiveType = environment.getTypeUtils().getPrimitiveType(candidate);
96-
return (primitiveType != null) ? primitiveType : candidate;
97-
}
98-
99-
private Object coerceValue(TypeMirror type, String value) {
100-
Object coercedValue = type.accept(DefaultValueCoercionTypeVisitor.INSTANCE, value);
101-
return (coercedValue != null) ? coercedValue : value;
47+
@Override
48+
protected List<Element> getDeprecatableElements() {
49+
return Arrays.asList(getGetter(), this.setter, this.field);
10250
}
10351

104-
private static final class DefaultValueCoercionTypeVisitor extends TypeKindVisitor8<Object, String> {
105-
106-
private static final DefaultValueCoercionTypeVisitor INSTANCE = new DefaultValueCoercionTypeVisitor();
107-
108-
private <T extends Number> T parseNumber(String value, Function<String, T> parser,
109-
PrimitiveType primitiveType) {
110-
try {
111-
return parser.apply(value);
112-
}
113-
catch (NumberFormatException ex) {
114-
throw new IllegalArgumentException(
115-
String.format("Invalid %s representation '%s'", primitiveType, value));
116-
}
117-
}
118-
119-
@Override
120-
public Object visitPrimitiveAsBoolean(PrimitiveType t, String value) {
121-
return Boolean.parseBoolean(value);
122-
}
123-
124-
@Override
125-
public Object visitPrimitiveAsByte(PrimitiveType t, String value) {
126-
return parseNumber(value, Byte::parseByte, t);
127-
}
128-
129-
@Override
130-
public Object visitPrimitiveAsShort(PrimitiveType t, String value) {
131-
return parseNumber(value, Short::parseShort, t);
132-
}
133-
134-
@Override
135-
public Object visitPrimitiveAsInt(PrimitiveType t, String value) {
136-
return parseNumber(value, Integer::parseInt, t);
137-
}
138-
139-
@Override
140-
public Object visitPrimitiveAsLong(PrimitiveType t, String value) {
141-
return parseNumber(value, Long::parseLong, t);
142-
}
143-
144-
@Override
145-
public Object visitPrimitiveAsChar(PrimitiveType t, String value) {
146-
if (value.length() > 1) {
147-
throw new IllegalArgumentException(String.format("Invalid character representation '%s'", value));
148-
}
149-
return value;
150-
}
151-
152-
@Override
153-
public Object visitPrimitiveAsFloat(PrimitiveType t, String value) {
154-
return parseNumber(value, Float::parseFloat, t);
155-
}
156-
157-
@Override
158-
public Object visitPrimitiveAsDouble(PrimitiveType t, String value) {
159-
return parseNumber(value, Double::parseDouble, t);
160-
}
161-
52+
@Override
53+
protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) {
54+
return environment.getNestedConfigurationPropertyAnnotation(this.field) != null;
16255
}
16356

164-
private static final class DefaultPrimitiveTypeVisitor extends TypeKindVisitor8<Object, Void> {
165-
166-
private static final DefaultPrimitiveTypeVisitor INSTANCE = new DefaultPrimitiveTypeVisitor();
167-
168-
@Override
169-
public Object visitPrimitiveAsBoolean(PrimitiveType t, Void ignore) {
170-
return false;
171-
}
172-
173-
@Override
174-
public Object visitPrimitiveAsByte(PrimitiveType t, Void ignore) {
175-
return (byte) 0;
176-
}
177-
178-
@Override
179-
public Object visitPrimitiveAsShort(PrimitiveType t, Void ignore) {
180-
return (short) 0;
181-
}
182-
183-
@Override
184-
public Object visitPrimitiveAsInt(PrimitiveType t, Void ignore) {
185-
return 0;
186-
}
187-
188-
@Override
189-
public Object visitPrimitiveAsLong(PrimitiveType t, Void ignore) {
190-
return 0L;
191-
}
192-
193-
@Override
194-
public Object visitPrimitiveAsChar(PrimitiveType t, Void ignore) {
195-
return null;
196-
}
197-
198-
@Override
199-
public Object visitPrimitiveAsFloat(PrimitiveType t, Void ignore) {
200-
return 0F;
201-
}
202-
203-
@Override
204-
public Object visitPrimitiveAsDouble(PrimitiveType t, Void ignore) {
205-
return 0D;
206-
}
207-
57+
@Override
58+
protected String resolveDescription(MetadataGenerationEnvironment environment) {
59+
return environment.getTypeUtils().getJavaDoc(this.field);
20860
}
20961

21062
}

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

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.boot.configurationprocessor;
1818

19+
import java.util.Arrays;
20+
import java.util.List;
21+
22+
import javax.lang.model.element.Element;
1923
import javax.lang.model.element.ExecutableElement;
2024
import javax.lang.model.element.TypeElement;
2125
import javax.lang.model.element.VariableElement;
@@ -25,23 +29,54 @@
2529
* A {@link PropertyDescriptor} for a standard JavaBean property.
2630
*
2731
* @author Stephane Nicoll
32+
* @author Phillip Webb
2833
*/
29-
class JavaBeanPropertyDescriptor extends PropertyDescriptor<ExecutableElement> {
34+
class JavaBeanPropertyDescriptor extends PropertyDescriptor {
35+
36+
private final ExecutableElement setter;
37+
38+
private final VariableElement field;
3039

31-
JavaBeanPropertyDescriptor(TypeElement ownerElement, ExecutableElement factoryMethod, ExecutableElement getter,
32-
String name, TypeMirror type, VariableElement field, ExecutableElement setter) {
33-
super(ownerElement, factoryMethod, getter, name, type, field, getter, setter);
40+
private final ExecutableElement factoryMethod;
41+
42+
JavaBeanPropertyDescriptor(String name, TypeMirror type, TypeElement declaringElement, ExecutableElement getter,
43+
ExecutableElement setter, VariableElement field, ExecutableElement factoryMethod) {
44+
super(name, type, declaringElement, getter);
45+
this.setter = setter;
46+
this.field = field;
47+
this.factoryMethod = factoryMethod;
48+
}
49+
50+
ExecutableElement getSetter() {
51+
return this.setter;
3452
}
3553

3654
@Override
37-
protected boolean isProperty(MetadataGenerationEnvironment env) {
38-
boolean isCollection = env.getTypeUtils().isCollectionOrMap(getType());
39-
return !env.isExcluded(getType()) && getGetter() != null && (getSetter() != null || isCollection);
55+
protected boolean isMarkedAsNested(MetadataGenerationEnvironment environment) {
56+
return environment.getNestedConfigurationPropertyAnnotation(this.field) != null;
57+
}
58+
59+
@Override
60+
protected String resolveDescription(MetadataGenerationEnvironment environment) {
61+
return environment.getTypeUtils().getJavaDoc(this.field);
4062
}
4163

4264
@Override
4365
protected Object resolveDefaultValue(MetadataGenerationEnvironment environment) {
44-
return environment.getFieldDefaultValue(getOwnerElement(), getName());
66+
return environment.getFieldDefaultValue(getDeclaringElement(), getName());
67+
}
68+
69+
@Override
70+
protected List<Element> getDeprecatableElements() {
71+
return Arrays.asList(getGetter(), this.setter, this.field, this.factoryMethod);
72+
}
73+
74+
@Override
75+
public boolean isProperty(MetadataGenerationEnvironment env) {
76+
boolean isCollection = env.getTypeUtils().isCollectionOrMap(getType());
77+
boolean hasGetter = getGetter() != null;
78+
boolean hasSetter = getSetter() != null;
79+
return !env.isExcluded(getType()) && hasGetter && (hasSetter || isCollection);
4580
}
4681

4782
}

0 commit comments

Comments
 (0)