Skip to content

Commit 3bcf7bc

Browse files
committed
Add support for including simple lists spring-projectsgh-16381
1 parent c859211 commit 3bcf7bc

File tree

2 files changed

+204
-3
lines changed

2 files changed

+204
-3
lines changed

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

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.io.Reader;
21+
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.Collection;
2324
import java.util.Collections;
@@ -69,6 +70,8 @@ public abstract class YamlProcessor {
6970

7071
private Set<String> supportedTypes = Collections.emptySet();
7172

73+
private boolean isIncludeSimpleLists = false;
74+
7275

7376
/**
7477
* A map of document matchers allowing callers to selectively use only
@@ -150,6 +153,73 @@ public void setSupportedTypes(Class<?>... supportedTypes) {
150153
}
151154
}
152155

156+
/**
157+
* Set the {@code isIncludeSimpleLists} flag, which enables adding the string
158+
* representation of simple lists/arrays to the {@code Properties} in addition
159+
* to the flattened indexed keys. When set to {@code false}, the default behavior
160+
* of just adding flattened keys is restored.
161+
* <p> For example, considering the following YAML snippet:
162+
* <pre><code>
163+
* animals:
164+
* mammals:
165+
* - cat
166+
* - dog
167+
* - horse
168+
* unicorns: []
169+
* </code></pre>
170+
* By default ({@code isAddFulllists == false}) the following entries are
171+
* added to the properties:
172+
* <pre><code>
173+
* animals.mammals[0]="cat"
174+
* animals.mammals[1]="dog"
175+
* animals.mammals[2]="horse"
176+
* animals.unicorns="" //an empty String
177+
* </code></pre>
178+
* With the flag set to {@code true}, an additional {@code animals.mammals}
179+
* key is added with the {@code List} representation of all elements.
180+
* The properties become:
181+
* <pre><code>
182+
* animals.mammals[0]="cat"
183+
* animals.mammals[1]="dog"
184+
* animals.mammals[2]="horse"
185+
* animals.mammals="[cat,dog,horse]" // can be parsed as a List
186+
* animals.unicorns="[]" // String representation of an empty List
187+
* </code></pre>
188+
* <p>This flag is ignored at levels where a list is detected to
189+
* contain a nested list, array or map:
190+
* <code><pre>
191+
* all:
192+
* animals:
193+
* - name: cat
194+
* type: mammal
195+
* - name: dragon
196+
* type: imaginary
197+
* </pre></code>
198+
* The above YAML results in the following properties even if this flag is
199+
* set to {@code true}:
200+
* <code><pre>
201+
* all.animals[0].name=cat
202+
* all.animals[0].type=mammal
203+
* all.animals[1].name=dragon
204+
* all.animals[1].type=imaginary
205+
* </pre></code>
206+
*
207+
* @param isIncludeSimpleLists {@code true} to enabling adding full lists during
208+
* flattening, {@code false} to disable it (the default)
209+
* @since 6.0.7
210+
*/
211+
public void setIncludeSimpleLists(boolean isIncludeSimpleLists) {
212+
this.isIncludeSimpleLists = isIncludeSimpleLists;
213+
}
214+
215+
/**
216+
* @return the value of the {{@link #setIncludeSimpleLists(boolean) isAddFullLists flag}
217+
* @since 6.0.7
218+
*/
219+
protected boolean isIncludeSimpleLists() {
220+
return this.isIncludeSimpleLists;
221+
}
222+
153223
/**
154224
* Provide an opportunity for subclasses to process the Yaml parsed from the supplied
155225
* resources. Each resource is parsed in turn and the documents inside checked against
@@ -326,13 +396,32 @@ else if (value instanceof Map map) {
326396
else if (value instanceof Collection collection) {
327397
// Need a compound key
328398
if (collection.isEmpty()) {
329-
result.put(key, "");
399+
result.put(key, isIncludeSimpleLists() ? Collections.emptyList() : "");
330400
}
331401
else {
332402
int count = 0;
403+
List<String> subCollectionKeys = new ArrayList<>();
333404
for (Object object : collection) {
334-
buildFlattenedMap(result, Collections.singletonMap(
335-
"[" + (count++) + "]", object), key);
405+
String indexKeyPart = "[" + (count++) + "]";
406+
if (object instanceof Collection || object instanceof Map) {
407+
subCollectionKeys.add(indexKeyPart);
408+
}
409+
buildFlattenedMap(result, Collections.singletonMap(indexKeyPart, object), key);
410+
}
411+
if (isIncludeSimpleLists()) {
412+
if (!subCollectionKeys.isEmpty()) {
413+
if (this.logger.isDebugEnabled()) {
414+
this.logger.debug(key + " not added as a full list because it contains "
415+
+ "nested lists/maps at indexes " + subCollectionKeys.stream().collect(Collectors.joining(",")));
416+
}
417+
else {
418+
this.logger.warn(key + " not added as a full list because it contains "
419+
+ subCollectionKeys.size() + " nested lists/maps");
420+
}
421+
}
422+
else {
423+
result.put(key, collection);
424+
}
336425
}
337426
}
338427
}

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

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class YamlProcessorTests {
4848
};
4949

5050

51+
@Test
52+
void addFullListsDefaultsToFalse() {
53+
assertThat(this.processor.isIncludeSimpleLists()).isFalse();
54+
}
55+
5156
@Test
5257
void arrayConvertedToIndexedBeanReference() {
5358
setYaml("foo: bar\nbar: [1,2,3]");
@@ -61,6 +66,113 @@ void arrayConvertedToIndexedBeanReference() {
6166
assertThat(properties.getProperty("bar[1]")).isEqualTo("2");
6267
assertThat(properties.get("bar[2]")).isEqualTo(3);
6368
assertThat(properties.getProperty("bar[2]")).isEqualTo("3");
69+
assertThat(properties).doesNotContainKey("bar");
70+
});
71+
}
72+
73+
//gh-16381
74+
@Test
75+
void arrayConvertedToIndexedKeysAndFullList() {
76+
setYaml("""
77+
animals:
78+
mammals:
79+
- cat
80+
- dog
81+
- horse
82+
unicorns: []
83+
""");
84+
this.processor.setIncludeSimpleLists(true);
85+
86+
this.processor.process((properties, map) -> {
87+
assertThat(properties)
88+
.doesNotContainKey("animals")
89+
.containsOnlyKeys("animals.mammals",
90+
"animals.mammals[0]",
91+
"animals.mammals[1]",
92+
"animals.mammals[2]",
93+
"animals.unicorns");
94+
assertThat(properties.getProperty("animals.mammals[0]")).isEqualTo("cat");
95+
assertThat(properties.getProperty("animals.mammals[1]")).isEqualTo("dog");
96+
assertThat(properties.getProperty("animals.mammals[2]")).isEqualTo("horse");
97+
98+
@SuppressWarnings("unchecked") List<Object> allMammals = (List<Object>) properties.get("animals.mammals");
99+
assertThat(allMammals).isNotNull().containsExactly("cat", "dog", "horse");
100+
assertThat(properties.getProperty("animals.mammals")).as("full list String form")
101+
.isEqualTo("[cat, dog, horse]");
102+
103+
@SuppressWarnings("unchecked") List<Object> unicorns = (List<Object>) properties.get("animals.unicorns");
104+
assertThat(unicorns).isNotNull().isEmpty();
105+
assertThat(properties.getProperty("animals.unicorns")).as("empty List in String form")
106+
.isEqualTo("[]");
107+
});
108+
}
109+
110+
@Test
111+
void arrayWithNestedMapsNotIncludingFullLists() {
112+
//here we test that fullList isn't effected if the list contains a Map
113+
setYaml("""
114+
all:
115+
animals:
116+
- name: cat
117+
type: mammal
118+
- name: dragon
119+
type: imaginary
120+
""");
121+
this.processor.setIncludeSimpleLists(true);
122+
123+
this.processor.process((properties, map) -> {
124+
assertThat(properties)
125+
.doesNotContainKeys("all", "all.animals")
126+
.hasSize(4)
127+
.containsEntry("all.animals[0].name", "cat")
128+
.containsEntry("all.animals[0].type", "mammal")
129+
.containsEntry("all.animals[1].name", "dragon")
130+
.containsEntry("all.animals[1].type", "imaginary");
131+
});
132+
}
133+
134+
@Test
135+
void arrayWithNestedListsNotIncludingFullLists() {
136+
//here we test that fullList isn't effected if the list contains a Collection
137+
setYaml("""
138+
foo:
139+
bar:
140+
- 1
141+
- [2,3,4]
142+
- [cat,dog]
143+
""");
144+
this.processor.setIncludeSimpleLists(true);
145+
146+
this.processor.process((properties, map) -> {
147+
assertThat(properties).hasSize(8);
148+
assertThat(properties)
149+
.doesNotContainKeys("foo", "foo.bar")
150+
.containsOnlyKeys(
151+
"foo.bar[0]",
152+
"foo.bar[1][0]",
153+
"foo.bar[1][1]",
154+
"foo.bar[1][2]",
155+
"foo.bar[2][0]",
156+
"foo.bar[2][1]",
157+
//full lists only for the last level of nesting
158+
"foo.bar[1]",
159+
"foo.bar[2]");
160+
assertThat(properties.get("foo.bar[0]")).isEqualTo(1);
161+
assertThat(properties.getProperty("foo.bar[0]")).isEqualTo("1");
162+
assertThat(properties.get("foo.bar[1][0]")).isEqualTo(2);
163+
assertThat(properties.getProperty("foo.bar[1][0]")).isEqualTo("2");
164+
assertThat(properties.get("foo.bar[1][2]")).isEqualTo(4);
165+
assertThat(properties.getProperty("foo.bar[1][2]")).isEqualTo("4");
166+
assertThat(properties.get("foo.bar[2][0]")).isEqualTo("cat");
167+
assertThat(properties.getProperty("foo.bar[2][0]")).isEqualTo("cat");
168+
169+
@SuppressWarnings("unchecked") List<Object> numbers = (List<Object>) properties.get("foo.bar[1]");
170+
assertThat(numbers).isNotNull().containsExactly(2, 3, 4);
171+
assertThat(properties.getProperty("foo.bar[1]")).isEqualTo("[2, 3, 4]");
172+
173+
@SuppressWarnings("unchecked") List<Object> animals = (List<Object>) properties.get("foo.bar[2]");
174+
assertThat(animals).isNotNull().containsExactly("cat", "dog");
175+
assertThat(properties.getProperty("foo.bar[2]")).isEqualTo("[cat, dog]");
64176
});
65177
}
66178

0 commit comments

Comments
 (0)