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 13648d824aa..fa393d32b36 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,13 +304,32 @@ public abstract class YamlProcessor { * @since 4.1.3 */ protected final Map getFlattenedMap(Map source) { + return getFlattenedMap(source, false); + } + + /** + * Return a flattened version of the given map, recursively following any nested Map + * or Collection values. Entries from the resulting map retain the same order as the + * 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 + * @return a flattened map + * @since 7.0.4 + */ + protected final Map getFlattenedMap(Map source, boolean includeNulls) { Map result = new LinkedHashMap<>(); - buildFlattenedMap(result, source, null); + buildFlattenedMap(result, source, null, includeNulls); return result; } @SuppressWarnings({"rawtypes", "unchecked"}) - private void buildFlattenedMap(Map result, Map source, @Nullable String path) { + private void buildFlattenedMap(Map result, Map source, @Nullable String path, + boolean includeNulls) { + if (includeNulls && source.isEmpty()) { + result.put(path, null); + return; + } source.forEach((key, value) -> { if (StringUtils.hasText(path)) { if (key.startsWith("[")) { @@ -325,7 +344,7 @@ public abstract class YamlProcessor { } else if (value instanceof Map map) { // Need a compound key - buildFlattenedMap(result, map, key); + buildFlattenedMap(result, map, key, includeNulls); } else if (value instanceof Collection collection) { // Need a compound key @@ -336,7 +355,7 @@ public abstract class YamlProcessor { int count = 0; for (Object object : collection) { buildFlattenedMap(result, Collections.singletonMap( - "[" + (count++) + "]", object), key); + "[" + (count++) + "]", object), key, includeNulls); } } } 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 b0cf6fa7be7..a134b6bf697 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 @@ -44,8 +44,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.set; */ class YamlProcessorTests { - private final YamlProcessor processor = new YamlProcessor() { - }; + private final TestYamlProcessor processor = new TestYamlProcessor(); @Test @@ -182,8 +181,33 @@ class YamlProcessorTests { .withMessageContaining("Global tag is not allowed: tag:yaml.org,2002:java.net.URL"); } + @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); + } + + @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); + } + private void setYaml(String yaml) { this.processor.setResources(new ByteArrayResource(yaml.getBytes())); } + private static class TestYamlProcessor extends YamlProcessor { + + Map processAndFlatten(boolean includeNulls) { + Map flattened = new LinkedHashMap<>(); + process((properties, map) -> flattened.putAll(getFlattenedMap(map, includeNulls))); + return flattened; + } + + + } + }