Skip to content

Commit 9975ec8

Browse files
committed
Report deprecated Map properties
Closes gh-27854
1 parent b7ae9fb commit 9975ec8

File tree

6 files changed

+145
-30
lines changed

6 files changed

+145
-30
lines changed

spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReport.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2023 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.
@@ -23,12 +23,11 @@
2323
import java.util.function.Function;
2424
import java.util.stream.Collectors;
2525

26-
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
27-
2826
/**
2927
* Provides a properties migration report.
3028
*
3129
* @author Stephane Nicoll
30+
* @author Moritz Halbritter
3231
*/
3332
class PropertiesMigrationReport {
3433

@@ -87,8 +86,7 @@ private void append(StringBuilder report, Map<String, List<PropertyMigration>> c
8786
report.append(String.format("Property source '%s':%n", name));
8887
properties.sort(PropertyMigration.COMPARATOR);
8988
properties.forEach((property) -> {
90-
ConfigurationMetadataProperty metadata = property.getMetadata();
91-
report.append(String.format("\tKey: %s%n", metadata.getId()));
89+
report.append(String.format("\tKey: %s%n", property.getProperty().getName()));
9290
if (property.getLineNumber() != null) {
9391
report.append(String.format("\t\tLine: %d%n", property.getLineNumber()));
9492
}

spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 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.
@@ -29,6 +29,7 @@
2929
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
3030
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
3131
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
32+
import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource;
3233
import org.springframework.boot.env.OriginTrackedMapPropertySource;
3334
import org.springframework.boot.origin.OriginTrackedValue;
3435
import org.springframework.core.env.ConfigurableEnvironment;
@@ -41,6 +42,7 @@
4142
* Report on {@link PropertyMigration properties migration}.
4243
*
4344
* @author Stephane Nicoll
45+
* @author Moritz Halbritter
4446
*/
4547
class PropertiesMigrationReporter {
4648

@@ -88,24 +90,40 @@ private PropertySource<?> mapPropertiesWithReplacement(PropertiesMigrationReport
8890
for (PropertyMigration candidate : renamed) {
8991
OriginTrackedValue value = OriginTrackedValue.of(candidate.getProperty().getValue(),
9092
candidate.getProperty().getOrigin());
91-
content.put(candidate.getMetadata().getDeprecation().getReplacement(), value);
93+
content.put(candidate.getNewPropertyName(), value);
9294
}
9395
return new OriginTrackedMapPropertySource(target, content);
9496
}
9597

98+
private boolean isMapType(ConfigurationMetadataProperty property) {
99+
String type = property.getType();
100+
return type != null && type.startsWith(Map.class.getName());
101+
}
102+
96103
private Map<String, List<PropertyMigration>> getMatchingProperties(
97104
Predicate<ConfigurationMetadataProperty> filter) {
98105
MultiValueMap<String, PropertyMigration> result = new LinkedMultiValueMap<>();
99106
List<ConfigurationMetadataProperty> candidates = this.allProperties.values().stream().filter(filter)
100107
.collect(Collectors.toList());
101-
getPropertySourcesAsMap().forEach((name, source) -> candidates.forEach((metadata) -> {
108+
getPropertySourcesAsMap().forEach((propertySourceName, propertySource) -> candidates.forEach((metadata) -> {
102109
ConfigurationPropertyName metadataName = ConfigurationPropertyName.isValid(metadata.getId())
103110
? ConfigurationPropertyName.of(metadata.getId())
104111
: ConfigurationPropertyName.adapt(metadata.getId(), '.');
105-
ConfigurationProperty configurationProperty = source.getConfigurationProperty(metadataName);
106-
if (configurationProperty != null) {
107-
result.add(name,
108-
new PropertyMigration(configurationProperty, metadata, determineReplacementMetadata(metadata)));
112+
// Direct match
113+
ConfigurationProperty match = propertySource.getConfigurationProperty(metadataName);
114+
if (match != null) {
115+
result.add(propertySourceName,
116+
new PropertyMigration(match, metadata, determineReplacementMetadata(metadata), false));
117+
}
118+
// Prefix match for maps
119+
if (isMapType(metadata) && propertySource instanceof IterableConfigurationPropertySource) {
120+
IterableConfigurationPropertySource iterableSource = (IterableConfigurationPropertySource) propertySource;
121+
iterableSource.stream().filter(metadataName::isAncestorOf).map(propertySource::getConfigurationProperty)
122+
.forEach((property) -> {
123+
ConfigurationMetadataProperty replacement = determineReplacementMetadata(metadata);
124+
result.add(propertySourceName,
125+
new PropertyMigration(property, metadata, replacement, true));
126+
});
109127
}
110128
}));
111129
return result;
@@ -125,8 +143,12 @@ private ConfigurationMetadataProperty determineReplacementMetadata(Configuration
125143

126144
private ConfigurationMetadataProperty detectMapValueReplacement(String fullId) {
127145
int lastDot = fullId.lastIndexOf('.');
128-
if (lastDot != -1) {
129-
return this.allProperties.get(fullId.substring(0, lastDot));
146+
if (lastDot == -1) {
147+
return null;
148+
}
149+
ConfigurationMetadataProperty metadata = this.allProperties.get(fullId.substring(0, lastDot));
150+
if (metadata != null && isMapType(metadata)) {
151+
return metadata;
130152
}
131153
return null;
132154
}

spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2023 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.
@@ -23,14 +23,17 @@
2323
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
2424
import org.springframework.boot.configurationmetadata.Deprecation;
2525
import org.springframework.boot.context.properties.source.ConfigurationProperty;
26+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
2627
import org.springframework.boot.origin.Origin;
2728
import org.springframework.boot.origin.TextResourceOrigin;
29+
import org.springframework.util.Assert;
2830
import org.springframework.util.StringUtils;
2931

3032
/**
3133
* Description of a property migration.
3234
*
3335
* @author Stephane Nicoll
36+
* @author Moritz Halbritter
3437
*/
3538
class PropertyMigration {
3639

@@ -45,14 +48,20 @@ class PropertyMigration {
4548

4649
private final ConfigurationMetadataProperty replacementMetadata;
4750

51+
/**
52+
* Whether this migration happened from a map type.
53+
*/
54+
private final boolean mapMigration;
55+
4856
private final boolean compatibleType;
4957

5058
PropertyMigration(ConfigurationProperty property, ConfigurationMetadataProperty metadata,
51-
ConfigurationMetadataProperty replacementMetadata) {
59+
ConfigurationMetadataProperty replacementMetadata, boolean mapMigration) {
5260
this.property = property;
5361
this.lineNumber = determineLineNumber(property);
5462
this.metadata = metadata;
5563
this.replacementMetadata = replacementMetadata;
64+
this.mapMigration = mapMigration;
5665
this.compatibleType = determineCompatibleType(metadata, replacementMetadata);
5766
}
5867

@@ -69,9 +78,9 @@ private static Integer determineLineNumber(ConfigurationProperty property) {
6978

7079
private static boolean determineCompatibleType(ConfigurationMetadataProperty metadata,
7180
ConfigurationMetadataProperty replacementMetadata) {
72-
String currentType = metadata.getType();
73-
String replacementType = determineReplacementType(replacementMetadata);
74-
if (replacementType == null || currentType == null) {
81+
String currentType = determineType(metadata);
82+
String replacementType = determineType(replacementMetadata);
83+
if (currentType == null || replacementType == null) {
7584
return false;
7685
}
7786
if (replacementType.equals(currentType)) {
@@ -84,14 +93,15 @@ private static boolean determineCompatibleType(ConfigurationMetadataProperty met
8493
return false;
8594
}
8695

87-
private static String determineReplacementType(ConfigurationMetadataProperty replacementMetadata) {
88-
if (replacementMetadata == null || replacementMetadata.getType() == null) {
96+
private static String determineType(ConfigurationMetadataProperty metadata) {
97+
if (metadata == null || metadata.getType() == null) {
8998
return null;
9099
}
91-
String candidate = replacementMetadata.getType();
100+
String candidate = metadata.getType();
92101
if (candidate.startsWith(Map.class.getName())) {
93102
int lastComma = candidate.lastIndexOf(',');
94103
if (lastComma != -1) {
104+
// Use Map value type
95105
return candidate.substring(lastComma + 1, candidate.length() - 1).trim();
96106
}
97107
}
@@ -114,9 +124,16 @@ boolean isCompatibleType() {
114124
return this.compatibleType;
115125
}
116126

127+
String getNewPropertyName() {
128+
if (this.mapMigration) {
129+
return getNewMapPropertyName(this.property, this.metadata, this.replacementMetadata).toString();
130+
}
131+
return this.metadata.getDeprecation().getReplacement();
132+
}
133+
117134
String determineReason() {
118135
if (this.compatibleType) {
119-
return "Replacement: " + this.metadata.getDeprecation().getReplacement();
136+
return "Replacement: " + getNewPropertyName();
120137
}
121138
Deprecation deprecation = this.metadata.getDeprecation();
122139
if (StringUtils.hasText(deprecation.getShortReason())) {
@@ -132,4 +149,14 @@ String determineReason() {
132149
return "Reason: none";
133150
}
134151

152+
private static ConfigurationPropertyName getNewMapPropertyName(ConfigurationProperty property,
153+
ConfigurationMetadataProperty metadata, ConfigurationMetadataProperty replacement) {
154+
ConfigurationPropertyName oldName = property.getName();
155+
ConfigurationPropertyName oldPrefix = ConfigurationPropertyName.of(metadata.getId());
156+
Assert.state(oldPrefix.isAncestorOf(oldName),
157+
String.format("'%s' is not an ancestor of '%s'", oldPrefix, oldName));
158+
ConfigurationPropertyName newPrefix = ConfigurationPropertyName.of(replacement.getId());
159+
return newPrefix.append(oldName.subName(oldPrefix.getNumberOfElements()));
160+
}
161+
135162
}

spring-boot-project/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporterTests.java

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 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.
@@ -46,10 +46,11 @@
4646
* Tests for {@link PropertiesMigrationReporter}.
4747
*
4848
* @author Stephane Nicoll
49+
* @author Moritz Halbritter
4950
*/
5051
class PropertiesMigrationReporterTests {
5152

52-
private ConfigurableEnvironment environment = new MockEnvironment();
53+
private final ConfigurableEnvironment environment = new MockEnvironment();
5354

5455
@Test
5556
void reportIsNullWithNoMatchingKeys() {
@@ -67,7 +68,11 @@ void replacementKeysAreRemapped() throws IOException {
6768
assertThat(propertySources).hasSize(3);
6869
createAnalyzer(loadRepository("metadata/sample-metadata.json")).getReport();
6970
assertThat(mapToNames(propertySources)).containsExactly("one", "migrate-two", "two", "mockProperties");
70-
assertMappedProperty(propertySources.get("migrate-two"), "test.two", "another", getOrigin(two, "wrong.two"));
71+
PropertySource<?> migrateTwo = propertySources.get("migrate-two");
72+
assertThat(migrateTwo).isNotNull();
73+
assertMappedProperty(migrateTwo, "test.two", "another", getOrigin(two, "wrong.two"));
74+
assertMappedProperty(migrateTwo, "custom.the-map-replacement.key", "value",
75+
getOrigin(two, "custom.map-with-replacement.key"));
7176
}
7277

7378
@Test
@@ -76,8 +81,9 @@ void warningReport() throws IOException {
7681
this.environment.getPropertySources().addFirst(loadPropertySource("ignore", "config/config-error.properties"));
7782
String report = createWarningReport(loadRepository("metadata/sample-metadata.json"));
7883
assertThat(report).isNotNull();
79-
assertThat(report).containsSubsequence("Property source 'test'", "wrong.four.test", "Line: 5", "test.four.test",
80-
"wrong.two", "Line: 2", "test.two");
84+
assertThat(report).containsSubsequence("Property source 'test'", "Key: custom.map-with-replacement.key",
85+
"Line: 8", "Replacement: custom.the-map-replacement.key", "wrong.four.test", "Line: 5",
86+
"test.four.test", "wrong.two", "Line: 2", "test.two");
8187
assertThat(report).doesNotContain("wrong.one");
8288
}
8389

@@ -119,6 +125,7 @@ void durationTypeIsHandledTransparently() {
119125
"test.time-to-live-ms", "test.time-to-live", "test.ttl", "test.mapped.ttl");
120126
assertThat(mapToNames(propertySources)).containsExactly("migrate-test", "test", "mockProperties");
121127
PropertySource<?> propertySource = propertySources.get("migrate-test");
128+
assertThat(propertySource).isNotNull();
122129
assertMappedProperty(propertySource, "test.cache", 50, null);
123130
assertMappedProperty(propertySource, "test.time-to-live", 1234L, null);
124131
assertMappedProperty(propertySource, "test.mapped.ttl", 5678L, null);
@@ -151,7 +158,47 @@ void invalidNameHandledGracefully() {
151158
.addFirst(new MapPropertySource("first", Collections.singletonMap("invalid.property-name", "value")));
152159
String report = createWarningReport(loadRepository("metadata/sample-metadata-invalid-name.json"));
153160
assertThat(report).isNotNull();
154-
assertThat(report).contains("Key: invalid.PropertyName").contains("Replacement: valid.property-name");
161+
assertThat(report).contains("Key: invalid.propertyname").contains("Replacement: valid.property-name");
162+
}
163+
164+
@Test
165+
void mapPropertiesDeprecatedNoReplacement() throws IOException {
166+
this.environment.getPropertySources().addFirst(
167+
new MapPropertySource("first", Collections.singletonMap("custom.map-no-replacement.key", "value")));
168+
String report = createErrorReport(loadRepository("metadata/sample-metadata.json"));
169+
assertThat(report).isNotNull();
170+
assertThat(report).contains("Key: custom.map-no-replacement.key")
171+
.contains("Reason: This is no longer supported.");
172+
}
173+
174+
@Test
175+
void mapPropertiesDeprecatedWithReplacement() throws IOException {
176+
this.environment.getPropertySources().addFirst(
177+
new MapPropertySource("first", Collections.singletonMap("custom.map-with-replacement.key", "value")));
178+
String report = createWarningReport(loadRepository("metadata/sample-metadata.json"));
179+
assertThat(report).isNotNull();
180+
assertThat(report).contains("Key: custom.map-with-replacement.key")
181+
.contains("Replacement: custom.the-map-replacement.key");
182+
}
183+
184+
@Test
185+
void mapPropertiesDeprecatedWithReplacementRelaxedBindingUnderscore() {
186+
this.environment.getPropertySources().addFirst(
187+
new MapPropertySource("first", Collections.singletonMap("custom.map_with_replacement.key", "value")));
188+
String report = createWarningReport(loadRepository("metadata/sample-metadata.json"));
189+
assertThat(report).isNotNull();
190+
assertThat(report).contains("Key: custom.mapwithreplacement.key")
191+
.contains("Replacement: custom.the-map-replacement.key");
192+
}
193+
194+
@Test
195+
void mapPropertiesDeprecatedWithReplacementRelaxedBindingCamelCase() {
196+
this.environment.getPropertySources().addFirst(
197+
new MapPropertySource("first", Collections.singletonMap("custom.MapWithReplacement.key", "value")));
198+
String report = createWarningReport(loadRepository("metadata/sample-metadata.json"));
199+
assertThat(report).isNotNull();
200+
assertThat(report).contains("Key: custom.mapwithreplacement.key")
201+
.contains("Replacement: custom.the-map-replacement.key");
155202
}
156203

157204
private List<String> mapToNames(PropertySources sources) {

spring-boot-project/spring-boot-properties-migrator/src/test/resources/config/config-warnings.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ wrong.two=another
33

44

55
wrong.four.test=value
6+
7+
8+
custom.map-with-replacement.key=value

spring-boot-project/spring-boot-properties-migrator/src/test/resources/metadata/sample-metadata.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@
3636
"replacement": "test.four.test",
3737
"level": "error"
3838
}
39+
},
40+
{
41+
"name": "custom.map-no-replacement",
42+
"type": "java.util.Map<java.lang.String,java.lang.String>",
43+
"deprecation": {
44+
"reason": "This is no longer supported."
45+
}
46+
},
47+
{
48+
"name": "custom.map-with-replacement",
49+
"type": "java.util.Map<java.lang.String,java.lang.String>",
50+
"deprecation": {
51+
"replacement": "custom.the-map-replacement"
52+
}
53+
},
54+
{
55+
"name": "custom.the-map-replacement",
56+
"type": "java.util.Map<java.lang.String,java.lang.String>"
3957
}
4058
]
41-
}
59+
}

0 commit comments

Comments
 (0)