Browse Source

Refine YamlProcessor to allow different empty values

Update `YamlProcessor` to allow any value to be inserted for
empty entries. This update will allow blank strings to be used
instead of `null` which better aligns with the way that
`.properties` files are loaded.

This update also allows allows the specified `emptyValue` to be
inserted instead of a blank String when the YAML contains `null`
or `~` values.

See gh-36197
See gh-19986

Signed-off-by: Phillip Webb <phil.webb@broadcom.com>
pull/36209/head
Phillip Webb 6 days ago committed by Juergen Hoeller
parent
commit
ab314aadd9
  1. 24
      spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java
  2. 34
      spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java

24
spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java

@ -304,7 +304,7 @@ public abstract class YamlProcessor { @@ -304,7 +304,7 @@ public abstract class YamlProcessor {
* @since 4.1.3
*/
protected final Map<String, Object> getFlattenedMap(Map<String, Object> source) {
return getFlattenedMap(source, false);
return getFlattenedMap(source, false, null);
}
/**
@ -313,21 +313,25 @@ public abstract class YamlProcessor { @@ -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<String, Object> getFlattenedMap(Map<String, Object> source, boolean includeNulls) {
protected final Map<String, Object> getFlattenedMap(Map<String, Object> source, boolean includeEmpty,
@Nullable Object emptyValue) {
Map<String, Object> result = new LinkedHashMap<>();
buildFlattenedMap(result, source, null, includeNulls);
buildFlattenedMap(result, source, null, includeEmpty, emptyValue);
return result;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void buildFlattenedMap(Map<String, Object> result, Map<String, Object> 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 { @@ -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 { @@ -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 : "")));
}
});
}

34
spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java

@ -183,16 +183,34 @@ class YamlProcessorTests { @@ -183,16 +183,34 @@ class YamlProcessorTests {
@Test
void processAndFlattenWithoutIncludedNulls() {
setYaml("foo: bar\nbar:\n spam: {}");
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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 { @@ -201,9 +219,9 @@ class YamlProcessorTests {
private static class TestYamlProcessor extends YamlProcessor {
Map<String, Object> processAndFlatten(boolean includeNulls) {
Map<String, Object> processAndFlatten(boolean includeEmpty, Object emptyValue) {
Map<String, Object> flattened = new LinkedHashMap<>();
process((properties, map) -> flattened.putAll(getFlattenedMap(map, includeNulls)));
process((properties, map) -> flattened.putAll(getFlattenedMap(map, includeEmpty, emptyValue)));
return flattened;
}

Loading…
Cancel
Save