diff --git a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java index 8e495f8eb116..231c51e3542f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java @@ -17,9 +17,11 @@ package org.springframework.boot.context.properties.migrator; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -86,14 +88,26 @@ private PropertySource mapPropertiesWithReplacement(PropertiesMigrationReport if (renamed.isEmpty()) { return null; } - String target = "migrate-" + name; - Map content = new LinkedHashMap<>(); - for (PropertyMigration candidate : renamed) { - OriginTrackedValue value = OriginTrackedValue.of(candidate.getProperty().getValue(), - candidate.getProperty().getOrigin()); - content.put(candidate.getNewPropertyName(), value); + NameTrackingPropertySource nameTrackingPropertySource = new NameTrackingPropertySource(); + this.environment.getPropertySources().addFirst(nameTrackingPropertySource); + try { + String target = "migrate-" + name; + Map content = new LinkedHashMap<>(); + for (PropertyMigration candidate : renamed) { + String newPropertyName = candidate.getNewPropertyName(); + Object value = candidate.getProperty().getValue(); + if (nameTrackingPropertySource.isPlaceholderThatAccessesName(value, newPropertyName)) { + continue; + } + OriginTrackedValue originTrackedValue = OriginTrackedValue.of(value, + candidate.getProperty().getOrigin()); + content.put(newPropertyName, originTrackedValue); + } + return new OriginTrackedMapPropertySource(target, content); + } + finally { + this.environment.getPropertySources().remove(nameTrackingPropertySource.getName()); } - return new OriginTrackedMapPropertySource(target, content); } private boolean isMapType(ConfigurationMetadataProperty property) { @@ -172,4 +186,33 @@ private String determinePropertySourceName(ConfigurationPropertySource source) { return source.getUnderlyingSource().toString(); } + /** + * {@link PropertySource} used to track accessed properties to protect against + * circular references. + */ + private class NameTrackingPropertySource extends PropertySource { + + private final Set accessedNames = new HashSet<>(); + + NameTrackingPropertySource() { + super(NameTrackingPropertySource.class.getName()); + } + + boolean isPlaceholderThatAccessesName(Object value, String name) { + if (value instanceof String) { + this.accessedNames.clear(); + PropertiesMigrationReporter.this.environment.resolvePlaceholders((String) value); + return this.accessedNames.contains(name); + } + return false; + } + + @Override + public Object getProperty(String name) { + this.accessedNames.add(name); + return null; + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporterTests.java index b6e513512805..3de49fe36667 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporterTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporterTests.java @@ -162,7 +162,7 @@ void invalidNameHandledGracefully() { } @Test - void mapPropertiesDeprecatedNoReplacement() throws IOException { + void mapPropertiesDeprecatedNoReplacement() { this.environment.getPropertySources() .addFirst( new MapPropertySource("first", Collections.singletonMap("custom.map-no-replacement.key", "value"))); @@ -173,7 +173,7 @@ void mapPropertiesDeprecatedNoReplacement() throws IOException { } @Test - void mapPropertiesDeprecatedWithReplacement() throws IOException { + void mapPropertiesDeprecatedWithReplacement() { this.environment.getPropertySources() .addFirst(new MapPropertySource("first", Collections.singletonMap("custom.map-with-replacement.key", "value"))); @@ -205,6 +205,14 @@ void mapPropertiesDeprecatedWithReplacementRelaxedBindingCamelCase() { .contains("Replacement: custom.the-map-replacement.key"); } + @Test // gh-35919 + void directCircularReference() { + this.environment.getPropertySources() + .addFirst(new MapPropertySource("backcompat", Collections.singletonMap("wrong.two", "${test.two}"))); + createAnalyzer(loadRepository("metadata/sample-metadata.json")).getReport(); + assertThat(this.environment.getProperty("test.two")).isNull(); + } + private List mapToNames(PropertySources sources) { List names = new ArrayList<>(); for (PropertySource source : sources) {