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
This commit is contained in:
@@ -93,7 +93,7 @@ If you use `@ConfigurationProperties` with record class then record components'
|
||||
|
||||
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.
|
||||
Also, the annotation processor cannot auto-detect default values for ``Collections``s.
|
||||
|
||||
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.
|
||||
Consider the following example:
|
||||
|
||||
@@ -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.
|
||||
@@ -36,6 +36,12 @@ class ExpressionTree extends ReflectionWrapper {
|
||||
|
||||
private final Method methodInvocationArgumentsMethod = findMethod(this.methodInvocationTreeType, "getArguments");
|
||||
|
||||
private final Class<?> memberSelectTreeType = findClass("com.sun.source.tree.MemberSelectTree");
|
||||
|
||||
private final Method memberSelectTreeExpressionMethod = findMethod(this.memberSelectTreeType, "getExpression");
|
||||
|
||||
private final Method memberSelectTreeIdentifierMethod = findMethod(this.memberSelectTreeType, "getIdentifier");
|
||||
|
||||
private final Class<?> newArrayTreeType = findClass("com.sun.source.tree.NewArrayTree");
|
||||
|
||||
private final Method arrayValueMethod = findMethod(this.newArrayTreeType, "getInitializers");
|
||||
@@ -65,6 +71,17 @@ class ExpressionTree extends ReflectionWrapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
Member getSelectedMember() throws Exception {
|
||||
if (this.memberSelectTreeType.isAssignableFrom(getInstance().getClass())) {
|
||||
String expression = this.memberSelectTreeExpressionMethod.invoke(getInstance()).toString();
|
||||
String identifier = this.memberSelectTreeIdentifierMethod.invoke(getInstance()).toString();
|
||||
if (expression != null && identifier != null) {
|
||||
return new Member(expression, identifier);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<? extends ExpressionTree> getArrayExpression() throws Exception {
|
||||
if (this.newArrayTreeType.isAssignableFrom(getInstance().getClass())) {
|
||||
List<?> elements = (List<?>) this.arrayValueMethod.invoke(getInstance());
|
||||
@@ -80,4 +97,7 @@ class ExpressionTree extends ReflectionWrapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
record Member(String expression, String identifier) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.configurationprocessor.fieldvalues.javac;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -27,6 +28,8 @@ import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
|
||||
import org.springframework.boot.configurationprocessor.fieldvalues.javac.ExpressionTree.Member;
|
||||
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
|
||||
|
||||
/**
|
||||
* {@link FieldValuesParser} implementation for the standard Java compiler.
|
||||
@@ -165,12 +168,12 @@ public class JavaCompilerFieldValuesParser implements FieldValuesParser {
|
||||
Class<?> wrapperType = WRAPPER_TYPES.get(variable.getType());
|
||||
Object defaultValue = DEFAULT_TYPE_VALUES.get(wrapperType);
|
||||
if (initializer != null) {
|
||||
return getValue(initializer, defaultValue);
|
||||
return getValue(variable.getType(), initializer, defaultValue);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private Object getValue(ExpressionTree expression, Object defaultValue) throws Exception {
|
||||
private Object getValue(String variableType, ExpressionTree expression, Object defaultValue) throws Exception {
|
||||
Object literalValue = expression.getLiteralValue();
|
||||
if (literalValue != null) {
|
||||
return literalValue;
|
||||
@@ -183,7 +186,7 @@ public class JavaCompilerFieldValuesParser implements FieldValuesParser {
|
||||
if (arrayValues != null) {
|
||||
Object[] result = new Object[arrayValues.size()];
|
||||
for (int i = 0; i < arrayValues.size(); i++) {
|
||||
Object value = getValue(arrayValues.get(i), null);
|
||||
Object value = getValue(variableType, arrayValues.get(i), null);
|
||||
if (value == null) { // One of the elements could not be resolved
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -195,7 +198,16 @@ public class JavaCompilerFieldValuesParser implements FieldValuesParser {
|
||||
return this.staticFinals.get(expression.toString());
|
||||
}
|
||||
if (expression.getKind().equals("MEMBER_SELECT")) {
|
||||
return WELL_KNOWN_STATIC_FINALS.get(expression.toString());
|
||||
Object value = WELL_KNOWN_STATIC_FINALS.get(expression.toString());
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
Member selectedMember = expression.getSelectedMember();
|
||||
// Type matching the expression, assuming an enum
|
||||
if (selectedMember != null && selectedMember.expression().equals(variableType)) {
|
||||
return ConventionUtils.toDashedCase(selectedMember.identifier().toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
package org.springframework.boot.configurationprocessor.metadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
|
||||
|
||||
/**
|
||||
* Configuration meta-data.
|
||||
@@ -36,13 +34,6 @@ import java.util.Set;
|
||||
*/
|
||||
public class ConfigurationMetadata {
|
||||
|
||||
private static final Set<Character> SEPARATORS;
|
||||
|
||||
static {
|
||||
List<Character> chars = Arrays.asList('-', '_');
|
||||
SEPARATORS = Collections.unmodifiableSet(new HashSet<>(chars));
|
||||
}
|
||||
|
||||
private final Map<String, List<ItemMetadata>> items;
|
||||
|
||||
private final Map<String, List<ItemHint>> hints;
|
||||
@@ -184,31 +175,11 @@ public class ConfigurationMetadata {
|
||||
|
||||
public static String nestedPrefix(String prefix, String name) {
|
||||
String nestedPrefix = (prefix != null) ? prefix : "";
|
||||
String dashedName = toDashedCase(name);
|
||||
String dashedName = ConventionUtils.toDashedCase(name);
|
||||
nestedPrefix += nestedPrefix.isEmpty() ? dashedName : "." + dashedName;
|
||||
return nestedPrefix;
|
||||
}
|
||||
|
||||
static String toDashedCase(String name) {
|
||||
StringBuilder dashed = new StringBuilder();
|
||||
Character previous = null;
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char current = name.charAt(i);
|
||||
if (SEPARATORS.contains(current)) {
|
||||
dashed.append("-");
|
||||
}
|
||||
else if (Character.isUpperCase(current) && previous != null && !SEPARATORS.contains(previous)) {
|
||||
dashed.append("-").append(current);
|
||||
}
|
||||
else {
|
||||
dashed.append(current);
|
||||
}
|
||||
previous = current;
|
||||
|
||||
}
|
||||
return dashed.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> List<T> flattenValues(Map<?, List<T>> map) {
|
||||
List<T> content = new ArrayList<>();
|
||||
for (List<T> values : map.values()) {
|
||||
|
||||
@@ -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.
|
||||
@@ -22,6 +22,8 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
|
||||
|
||||
/**
|
||||
* Provide hints on an {@link ItemMetadata}. Defines the list of possible values for a
|
||||
* particular item as {@link ItemHint.ValueHint} instances.
|
||||
@@ -53,9 +55,9 @@ public class ItemHint implements Comparable<ItemHint> {
|
||||
if (dot != -1) {
|
||||
String prefix = name.substring(0, dot);
|
||||
String originalName = name.substring(dot);
|
||||
return prefix + ConfigurationMetadata.toDashedCase(originalName);
|
||||
return prefix + ConventionUtils.toDashedCase(originalName);
|
||||
}
|
||||
return ConfigurationMetadata.toDashedCase(name);
|
||||
return ConventionUtils.toDashedCase(name);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.boot.configurationprocessor.metadata;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.support.ConventionUtils;
|
||||
|
||||
/**
|
||||
* A group or property meta-data item from some {@link ConfigurationMetadata}.
|
||||
*
|
||||
@@ -68,7 +70,7 @@ public final class ItemMetadata implements Comparable<ItemMetadata> {
|
||||
if (!fullName.isEmpty()) {
|
||||
fullName.append('.');
|
||||
}
|
||||
fullName.append(ConfigurationMetadata.toDashedCase(name));
|
||||
fullName.append(ConventionUtils.toDashedCase(name));
|
||||
}
|
||||
return fullName.toString();
|
||||
}
|
||||
@@ -218,7 +220,7 @@ public final class ItemMetadata implements Comparable<ItemMetadata> {
|
||||
}
|
||||
|
||||
public static String newItemMetadataPrefix(String prefix, String suffix) {
|
||||
return prefix.toLowerCase(Locale.ENGLISH) + ConfigurationMetadata.toDashedCase(suffix);
|
||||
return prefix.toLowerCase(Locale.ENGLISH) + ConventionUtils.toDashedCase(suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Convention utilities.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public abstract class ConventionUtils {
|
||||
|
||||
private static final Set<Character> SEPARATORS;
|
||||
|
||||
static {
|
||||
List<Character> chars = Arrays.asList('-', '_');
|
||||
SEPARATORS = Collections.unmodifiableSet(new HashSet<>(chars));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the idiomatic metadata format for the given {@code value}.
|
||||
* @param value a value
|
||||
* @return the idiomatic format for the value, or the value itself if it already
|
||||
* complies with the idiomatic metadata format.
|
||||
*/
|
||||
public static String toDashedCase(String value) {
|
||||
StringBuilder dashed = new StringBuilder();
|
||||
Character previous = null;
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char current = value.charAt(i);
|
||||
if (SEPARATORS.contains(current)) {
|
||||
dashed.append("-");
|
||||
}
|
||||
else if (Character.isUpperCase(current) && previous != null && !SEPARATORS.contains(previous)) {
|
||||
dashed.append("-").append(current);
|
||||
}
|
||||
else {
|
||||
dashed.append(current);
|
||||
}
|
||||
previous = current;
|
||||
|
||||
}
|
||||
return dashed.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support classes for configuration metadata processing.
|
||||
*/
|
||||
package org.springframework.boot.configurationprocessor.support;
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.boot.configurationprocessor;
|
||||
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
|
||||
@@ -50,6 +53,7 @@ import org.springframework.boot.configurationsample.specific.DeprecatedSimplePoj
|
||||
import org.springframework.boot.configurationsample.specific.DeprecatedUnrelatedMethodPojo;
|
||||
import org.springframework.boot.configurationsample.specific.DoubleRegistrationProperties;
|
||||
import org.springframework.boot.configurationsample.specific.EmptyDefaultValueProperties;
|
||||
import org.springframework.boot.configurationsample.specific.EnumValuesPojo;
|
||||
import org.springframework.boot.configurationsample.specific.ExcludedTypesPojo;
|
||||
import org.springframework.boot.configurationsample.specific.InnerClassAnnotatedGetterConfig;
|
||||
import org.springframework.boot.configurationsample.specific.InnerClassHierarchicalProperties;
|
||||
@@ -173,6 +177,15 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene
|
||||
.fromSource(HierarchicalProperties.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void enumValues() {
|
||||
ConfigurationMetadata metadata = compile(EnumValuesPojo.class);
|
||||
assertThat(metadata).has(Metadata.withGroup("test").fromSource(EnumValuesPojo.class));
|
||||
assertThat(metadata).has(Metadata.withProperty("test.seconds", ChronoUnit.class).withDefaultValue("seconds"));
|
||||
assertThat(metadata)
|
||||
.has(Metadata.withProperty("test.hour-of-day", ChronoField.class).withDefaultValue("hour-of-day"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void descriptionProperties() {
|
||||
ConfigurationMetadata metadata = compile(DescriptionProperties.class);
|
||||
|
||||
@@ -48,7 +48,7 @@ public abstract class AbstractFieldValuesProcessorTests {
|
||||
protected abstract FieldValuesParser createProcessor(ProcessingEnvironment env);
|
||||
|
||||
@Test
|
||||
void getFieldValues() throws Exception {
|
||||
void getFieldValues() {
|
||||
TestProcessor processor = new TestProcessor();
|
||||
TestCompiler compiler = TestCompiler.forSystem()
|
||||
.withProcessors(processor)
|
||||
@@ -105,6 +105,11 @@ public abstract class AbstractFieldValuesProcessorTests {
|
||||
assertThat(values.get("periodMonths")).isEqualTo("10m");
|
||||
assertThat(values.get("periodYears")).isEqualTo("15y");
|
||||
assertThat(values.get("periodZero")).isEqualTo(0);
|
||||
assertThat(values.get("enumNone")).isNull();
|
||||
assertThat(values.get("enumSimple")).isEqualTo("seconds");
|
||||
assertThat(values.get("enumQualified")).isEqualTo("hour-of-day");
|
||||
assertThat(values.get("enumWithIndirection")).isNull();
|
||||
assertThat(values.get("memberSelectInt")).isNull();
|
||||
}
|
||||
|
||||
@SupportedAnnotationTypes({ "org.springframework.boot.configurationsample.ConfigurationProperties" })
|
||||
|
||||
@@ -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.
|
||||
@@ -14,18 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.configurationprocessor.metadata;
|
||||
package org.springframework.boot.configurationprocessor.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConfigurationMetadata}.
|
||||
* Tests for {@link ConventionUtils}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class ConfigurationMetadataTests {
|
||||
class ConventionUtilsTests {
|
||||
|
||||
@Test
|
||||
void toDashedCaseCamelCase() {
|
||||
@@ -78,7 +78,7 @@ class ConfigurationMetadataTests {
|
||||
}
|
||||
|
||||
private String toDashedCase(String name) {
|
||||
return ConfigurationMetadata.toDashedCase(name);
|
||||
return ConventionUtils.toDashedCase(name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 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.
|
||||
@@ -20,6 +20,7 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Period;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.springframework.boot.configurationsample.ConfigurationProperties;
|
||||
import org.springframework.util.MimeType;
|
||||
@@ -151,4 +152,22 @@ public class FieldValues {
|
||||
|
||||
private Period periodZero = Period.ZERO;
|
||||
|
||||
private ChronoUnit enumNone;
|
||||
|
||||
private ChronoUnit enumSimple = ChronoUnit.SECONDS;
|
||||
|
||||
private java.time.temporal.ChronoField enumQualified = java.time.temporal.ChronoField.HOUR_OF_DAY;
|
||||
|
||||
private ChronoUnit enumWithIndirection = SampleOptions.DEFAULT_UNIT;
|
||||
|
||||
private int memberSelectInt = SampleOptions.DEFAULT_MAX_RETRIES;
|
||||
|
||||
public static class SampleOptions {
|
||||
|
||||
static final Integer DEFAULT_MAX_RETRIES = 20;
|
||||
|
||||
static final ChronoUnit DEFAULT_UNIT = ChronoUnit.SECONDS;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.specific;
|
||||
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.springframework.boot.configurationsample.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Sample config for enum and default values.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@ConfigurationProperties("test")
|
||||
public class EnumValuesPojo {
|
||||
|
||||
private ChronoUnit seconds = ChronoUnit.SECONDS;
|
||||
|
||||
private ChronoField hourOfDay = ChronoField.HOUR_OF_DAY;
|
||||
|
||||
public ChronoUnit getSeconds() {
|
||||
return this.seconds;
|
||||
}
|
||||
|
||||
public void setSeconds(ChronoUnit seconds) {
|
||||
this.seconds = seconds;
|
||||
}
|
||||
|
||||
public ChronoField getHourOfDay() {
|
||||
return this.hourOfDay;
|
||||
}
|
||||
|
||||
public void setHourOfDay(ChronoField hourOfDay) {
|
||||
this.hourOfDay = hourOfDay;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user