diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index fa393d32b36..31b46cf7186 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -304,7 +304,7 @@ public abstract class YamlProcessor { * @since 4.1.3 */ protected final Map getFlattenedMap(Map source) { - return getFlattenedMap(source, false); + return getFlattenedMap(source, false, null); } /** @@ -313,21 +313,25 @@ public abstract class YamlProcessor { * source. When called with the Map from a {@link MatchCallback} the result will * contain the same values as the {@link MatchCallback} Properties. * @param source the source map - * @param includeNulls if {@code null} entries can be included in the result + * @param includeEmpty if empty entries should be included in the result + * @param emptyValue the value used to represent empty entries (e.g. {@code null} or an empty {@code String} * @return a flattened map * @since 7.0.4 */ - protected final Map getFlattenedMap(Map source, boolean includeNulls) { + protected final Map getFlattenedMap(Map source, boolean includeEmpty, + @Nullable Object emptyValue) { + Map result = new LinkedHashMap<>(); - buildFlattenedMap(result, source, null, includeNulls); + buildFlattenedMap(result, source, null, includeEmpty, emptyValue); return result; } @SuppressWarnings({"rawtypes", "unchecked"}) private void buildFlattenedMap(Map result, Map source, @Nullable String path, - boolean includeNulls) { - if (includeNulls && source.isEmpty()) { - result.put(path, null); + boolean includeEmpty, @Nullable Object emptyValue) { + + if (includeEmpty && source.isEmpty()) { + result.put(path, emptyValue); return; } source.forEach((key, value) -> { @@ -344,7 +348,7 @@ public abstract class YamlProcessor { } else if (value instanceof Map map) { // Need a compound key - buildFlattenedMap(result, map, key, includeNulls); + buildFlattenedMap(result, map, key, includeEmpty, emptyValue); } else if (value instanceof Collection collection) { // Need a compound key @@ -355,12 +359,12 @@ public abstract class YamlProcessor { int count = 0; for (Object object : collection) { buildFlattenedMap(result, Collections.singletonMap( - "[" + (count++) + "]", object), key, includeNulls); + "[" + (count++) + "]", object), key, includeEmpty, emptyValue); } } } else { - result.put(key, (value != null ? value : "")); + result.put(key, (value != null ? value : (includeEmpty ? emptyValue : ""))); } }); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java index a134b6bf697..693df504b12 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java @@ -183,16 +183,34 @@ class YamlProcessorTests { @Test void processAndFlattenWithoutIncludedNulls() { - setYaml("foo: bar\nbar:\n spam: {}"); - Map flattened = this.processor.processAndFlatten(false); - assertThat(flattened).containsEntry("foo", "bar").doesNotContainKey("bar.spam").hasSize(1); + setYaml("avalue: value\nanull: null\natilde: ~\nparent:\n anempty: {}\n"); + Map flattened = this.processor.processAndFlatten(false, null); + assertThat(flattened).containsOnly( + entry("avalue", "value"), + entry("anull", ""), + entry("atilde", "")); } @Test void processAndFlattenWithIncludedNulls() { - setYaml("foo: bar\nbar:\n spam: {}"); - Map flattened = this.processor.processAndFlatten(true); - assertThat(flattened).containsEntry("foo", "bar").containsEntry("bar.spam", null).hasSize(2); + setYaml("avalue: value\nanull: null\natilde: ~\nparent:\n anempty: {}\n"); + Map flattened = this.processor.processAndFlatten(true, null); + assertThat(flattened).containsOnly( + entry("avalue", "value"), + entry("anull", null), + entry("atilde", null), + entry("parent.anempty", null)); + } + + @Test + void processAndFlattenWithIncludedBlankString() { + setYaml("avalue: value\nanull: null\natilde: ~\nparent:\n anempty: {}\n"); + Map flattened = this.processor.processAndFlatten(true, ""); + assertThat(flattened).containsOnly( + entry("avalue", "value"), + entry("anull", ""), + entry("atilde", ""), + entry("parent.anempty", "")); } private void setYaml(String yaml) { @@ -201,9 +219,9 @@ class YamlProcessorTests { private static class TestYamlProcessor extends YamlProcessor { - Map processAndFlatten(boolean includeNulls) { + Map processAndFlatten(boolean includeEmpty, Object emptyValue) { Map flattened = new LinkedHashMap<>(); - process((properties, map) -> flattened.putAll(getFlattenedMap(map, includeNulls))); + process((properties, map) -> flattened.putAll(getFlattenedMap(map, includeEmpty, emptyValue))); return flattened; }