Skip to content

Commit 477bd7d

Browse files
committed
Detect default enum value
This commit improves the configuration metadata annotation processor to detect a default enum value. The algorithm is best-effort, similarly to what it already does for well known prefixes (period, duration, etc). Based on an expression and an identifier, the default value is inferred if the expression matches the declaration of the property type. See gh-7562
1 parent f4b4f4f commit 477bd7d

File tree

13 files changed

+233
-50
lines changed

13 files changed

+233
-50
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ If you use `@ConfigurationProperties` with record class then record components'
9393

9494
The annotation processor applies a number of heuristics to extract the default value from the source model.
9595
Default values have to be provided statically. In particular, do not refer to a constant defined in another class.
96-
Also, the annotation processor cannot auto-detect default values for ``Enum``s and ``Collections``s.
96+
Also, the annotation processor cannot auto-detect default values for ``Collections``s.
9797

9898
For cases where the default value could not be detected, xref:configuration-metadata/annotation-processor.adoc#appendix.configuration-metadata.annotation-processor.adding-additional-metadata[manual metadata] should be provided.
9999
Consider the following example:

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java

Lines changed: 21 additions & 1 deletion
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.
@@ -36,6 +36,12 @@ class ExpressionTree extends ReflectionWrapper {
3636

3737
private final Method methodInvocationArgumentsMethod = findMethod(this.methodInvocationTreeType, "getArguments");
3838

39+
private final Class<?> memberSelectTreeType = findClass("com.sun.source.tree.MemberSelectTree");
40+
41+
private final Method memberSelectTreeExpressionMethod = findMethod(this.memberSelectTreeType, "getExpression");
42+
43+
private final Method memberSelectTreeIdentifierMethod = findMethod(this.memberSelectTreeType, "getIdentifier");
44+
3945
private final Class<?> newArrayTreeType = findClass("com.sun.source.tree.NewArrayTree");
4046

4147
private final Method arrayValueMethod = findMethod(this.newArrayTreeType, "getInitializers");
@@ -65,6 +71,17 @@ Object getFactoryValue() throws Exception {
6571
return null;
6672
}
6773

74+
Member getSelectedMember() throws Exception {
75+
if (this.memberSelectTreeType.isAssignableFrom(getInstance().getClass())) {
76+
String expression = this.memberSelectTreeExpressionMethod.invoke(getInstance()).toString();
77+
String identifier = this.memberSelectTreeIdentifierMethod.invoke(getInstance()).toString();
78+
if (expression != null && identifier != null) {
79+
return new Member(expression, identifier);
80+
}
81+
}
82+
return null;
83+
}
84+
6885
List<? extends ExpressionTree> getArrayExpression() throws Exception {
6986
if (this.newArrayTreeType.isAssignableFrom(getInstance().getClass())) {
7087
List<?> elements = (List<?>) this.arrayValueMethod.invoke(getInstance());
@@ -80,4 +97,7 @@ List<? extends ExpressionTree> getArrayExpression() throws Exception {
8097
return null;
8198
}
8299

100+
record Member(String expression, String identifier) {
101+
}
102+
83103
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Collections;
2020
import java.util.HashMap;
2121
import java.util.List;
22+
import java.util.Locale;
2223
import java.util.Map;
2324
import java.util.Set;
2425

@@ -27,6 +28,8 @@
2728
import javax.lang.model.element.TypeElement;
2829

2930
import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
31+
import org.springframework.boot.configurationprocessor.fieldvalues.javac.ExpressionTree.Member;
32+
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
3033

3134
/**
3235
* {@link FieldValuesParser} implementation for the standard Java compiler.
@@ -165,12 +168,12 @@ private Object getValue(VariableTree variable) throws Exception {
165168
Class<?> wrapperType = WRAPPER_TYPES.get(variable.getType());
166169
Object defaultValue = DEFAULT_TYPE_VALUES.get(wrapperType);
167170
if (initializer != null) {
168-
return getValue(initializer, defaultValue);
171+
return getValue(variable.getType(), initializer, defaultValue);
169172
}
170173
return defaultValue;
171174
}
172175

173-
private Object getValue(ExpressionTree expression, Object defaultValue) throws Exception {
176+
private Object getValue(String variableType, ExpressionTree expression, Object defaultValue) throws Exception {
174177
Object literalValue = expression.getLiteralValue();
175178
if (literalValue != null) {
176179
return literalValue;
@@ -183,7 +186,7 @@ private Object getValue(ExpressionTree expression, Object defaultValue) throws E
183186
if (arrayValues != null) {
184187
Object[] result = new Object[arrayValues.size()];
185188
for (int i = 0; i < arrayValues.size(); i++) {
186-
Object value = getValue(arrayValues.get(i), null);
189+
Object value = getValue(variableType, arrayValues.get(i), null);
187190
if (value == null) { // One of the elements could not be resolved
188191
return defaultValue;
189192
}
@@ -195,7 +198,16 @@ private Object getValue(ExpressionTree expression, Object defaultValue) throws E
195198
return this.staticFinals.get(expression.toString());
196199
}
197200
if (expression.getKind().equals("MEMBER_SELECT")) {
198-
return WELL_KNOWN_STATIC_FINALS.get(expression.toString());
201+
Object value = WELL_KNOWN_STATIC_FINALS.get(expression.toString());
202+
if (value != null) {
203+
return value;
204+
}
205+
Member selectedMember = expression.getSelectedMember();
206+
// Type matching the expression, assuming an enum
207+
if (selectedMember != null && selectedMember.expression().equals(variableType)) {
208+
return ConventionUtils.toDashedCase(selectedMember.identifier().toLowerCase(Locale.ENGLISH));
209+
}
210+
return null;
199211
}
200212
return defaultValue;
201213
}

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

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@
1717
package org.springframework.boot.configurationprocessor.metadata;
1818

1919
import java.util.ArrayList;
20-
import java.util.Arrays;
2120
import java.util.Collections;
22-
import java.util.HashSet;
2321
import java.util.LinkedHashMap;
2422
import java.util.List;
25-
import java.util.Locale;
2623
import java.util.Map;
27-
import java.util.Set;
24+
25+
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
2826

2927
/**
3028
* Configuration meta-data.
@@ -36,13 +34,6 @@
3634
*/
3735
public class ConfigurationMetadata {
3836

39-
private static final Set<Character> SEPARATORS;
40-
41-
static {
42-
List<Character> chars = Arrays.asList('-', '_');
43-
SEPARATORS = Collections.unmodifiableSet(new HashSet<>(chars));
44-
}
45-
4637
private final Map<String, List<ItemMetadata>> items;
4738

4839
private final Map<String, List<ItemHint>> hints;
@@ -184,31 +175,11 @@ private boolean nullSafeEquals(Object o1, Object o2) {
184175

185176
public static String nestedPrefix(String prefix, String name) {
186177
String nestedPrefix = (prefix != null) ? prefix : "";
187-
String dashedName = toDashedCase(name);
178+
String dashedName = ConventionUtils.toDashedCase(name);
188179
nestedPrefix += nestedPrefix.isEmpty() ? dashedName : "." + dashedName;
189180
return nestedPrefix;
190181
}
191182

192-
static String toDashedCase(String name) {
193-
StringBuilder dashed = new StringBuilder();
194-
Character previous = null;
195-
for (int i = 0; i < name.length(); i++) {
196-
char current = name.charAt(i);
197-
if (SEPARATORS.contains(current)) {
198-
dashed.append("-");
199-
}
200-
else if (Character.isUpperCase(current) && previous != null && !SEPARATORS.contains(previous)) {
201-
dashed.append("-").append(current);
202-
}
203-
else {
204-
dashed.append(current);
205-
}
206-
previous = current;
207-
208-
}
209-
return dashed.toString().toLowerCase(Locale.ENGLISH);
210-
}
211-
212183
private static <T extends Comparable<T>> List<T> flattenValues(Map<?, List<T>> map) {
213184
List<T> content = new ArrayList<>();
214185
for (List<T> values : map.values()) {

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

Lines changed: 5 additions & 3 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.
@@ -22,6 +22,8 @@
2222
import java.util.List;
2323
import java.util.Map;
2424

25+
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
26+
2527
/**
2628
* Provide hints on an {@link ItemMetadata}. Defines the list of possible values for a
2729
* particular item as {@link ItemHint.ValueHint} instances.
@@ -53,9 +55,9 @@ private String toCanonicalName(String name) {
5355
if (dot != -1) {
5456
String prefix = name.substring(0, dot);
5557
String originalName = name.substring(dot);
56-
return prefix + ConfigurationMetadata.toDashedCase(originalName);
58+
return prefix + ConventionUtils.toDashedCase(originalName);
5759
}
58-
return ConfigurationMetadata.toDashedCase(name);
60+
return ConventionUtils.toDashedCase(name);
5961
}
6062

6163
public String getName() {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.util.Locale;
2020

21+
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
22+
2123
/**
2224
* A group or property meta-data item from some {@link ConfigurationMetadata}.
2325
*
@@ -68,7 +70,7 @@ private String buildName(String prefix, String name) {
6870
if (!fullName.isEmpty()) {
6971
fullName.append('.');
7072
}
71-
fullName.append(ConfigurationMetadata.toDashedCase(name));
73+
fullName.append(ConventionUtils.toDashedCase(name));
7274
}
7375
return fullName.toString();
7476
}
@@ -218,7 +220,7 @@ public static ItemMetadata newProperty(String prefix, String name, String type,
218220
}
219221

220222
public static String newItemMetadataPrefix(String prefix, String suffix) {
221-
return prefix.toLowerCase(Locale.ENGLISH) + ConfigurationMetadata.toDashedCase(suffix);
223+
return prefix.toLowerCase(Locale.ENGLISH) + ConventionUtils.toDashedCase(suffix);
222224
}
223225

224226
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.configurationprocessor.support;
18+
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.HashSet;
22+
import java.util.List;
23+
import java.util.Locale;
24+
import java.util.Set;
25+
26+
/**
27+
* Convention utilities.
28+
*
29+
* @author Stephane Nicoll
30+
* @since 3.4.0
31+
*/
32+
public abstract class ConventionUtils {
33+
34+
private static final Set<Character> SEPARATORS;
35+
36+
static {
37+
List<Character> chars = Arrays.asList('-', '_');
38+
SEPARATORS = Collections.unmodifiableSet(new HashSet<>(chars));
39+
}
40+
41+
/**
42+
* Return the idiomatic metadata format for the given {@code value}.
43+
* @param value a value
44+
* @return the idiomatic format for the value, or the value itself if it already
45+
* complies with the idiomatic metadata format.
46+
*/
47+
public static String toDashedCase(String value) {
48+
StringBuilder dashed = new StringBuilder();
49+
Character previous = null;
50+
for (int i = 0; i < value.length(); i++) {
51+
char current = value.charAt(i);
52+
if (SEPARATORS.contains(current)) {
53+
dashed.append("-");
54+
}
55+
else if (Character.isUpperCase(current) && previous != null && !SEPARATORS.contains(previous)) {
56+
dashed.append("-").append(current);
57+
}
58+
else {
59+
dashed.append(current);
60+
}
61+
previous = current;
62+
63+
}
64+
return dashed.toString().toLowerCase(Locale.ENGLISH);
65+
}
66+
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Support classes for configuration metadata processing.
19+
*/
20+
package org.springframework.boot.configurationprocessor.support;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.boot.configurationprocessor;
1818

19+
import java.time.temporal.ChronoField;
20+
import java.time.temporal.ChronoUnit;
21+
1922
import org.junit.jupiter.api.Test;
2023

2124
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
@@ -50,6 +53,7 @@
5053
import org.springframework.boot.configurationsample.specific.DeprecatedUnrelatedMethodPojo;
5154
import org.springframework.boot.configurationsample.specific.DoubleRegistrationProperties;
5255
import org.springframework.boot.configurationsample.specific.EmptyDefaultValueProperties;
56+
import org.springframework.boot.configurationsample.specific.EnumValuesPojo;
5357
import org.springframework.boot.configurationsample.specific.ExcludedTypesPojo;
5458
import org.springframework.boot.configurationsample.specific.InnerClassAnnotatedGetterConfig;
5559
import org.springframework.boot.configurationsample.specific.InnerClassHierarchicalProperties;
@@ -173,6 +177,15 @@ void hierarchicalProperties() {
173177
.fromSource(HierarchicalProperties.class));
174178
}
175179

180+
@Test
181+
void enumValues() {
182+
ConfigurationMetadata metadata = compile(EnumValuesPojo.class);
183+
assertThat(metadata).has(Metadata.withGroup("test").fromSource(EnumValuesPojo.class));
184+
assertThat(metadata).has(Metadata.withProperty("test.seconds", ChronoUnit.class).withDefaultValue("seconds"));
185+
assertThat(metadata)
186+
.has(Metadata.withProperty("test.hour-of-day", ChronoField.class).withDefaultValue("hour-of-day"));
187+
}
188+
176189
@Test
177190
void descriptionProperties() {
178191
ConfigurationMetadata metadata = compile(DescriptionProperties.class);

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public abstract class AbstractFieldValuesProcessorTests {
4848
protected abstract FieldValuesParser createProcessor(ProcessingEnvironment env);
4949

5050
@Test
51-
void getFieldValues() throws Exception {
51+
void getFieldValues() {
5252
TestProcessor processor = new TestProcessor();
5353
TestCompiler compiler = TestCompiler.forSystem()
5454
.withProcessors(processor)
@@ -105,6 +105,11 @@ void getFieldValues() throws Exception {
105105
assertThat(values.get("periodMonths")).isEqualTo("10m");
106106
assertThat(values.get("periodYears")).isEqualTo("15y");
107107
assertThat(values.get("periodZero")).isEqualTo(0);
108+
assertThat(values.get("enumNone")).isNull();
109+
assertThat(values.get("enumSimple")).isEqualTo("seconds");
110+
assertThat(values.get("enumQualified")).isEqualTo("hour-of-day");
111+
assertThat(values.get("enumWithIndirection")).isNull();
112+
assertThat(values.get("memberSelectInt")).isNull();
108113
}
109114

110115
@SupportedAnnotationTypes({ "org.springframework.boot.configurationsample.ConfigurationProperties" })

0 commit comments

Comments
 (0)