Skip to content

Commit 0957168

Browse files
committed
Binding to collection of custom objects should not fail with unbound error
Fixes gh-20134
1 parent fb97f07 commit 0957168

File tree

2 files changed

+150
-1
lines changed

2 files changed

+150
-1
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public class NoUnboundElementsBindHandler extends AbstractBindHandler {
4343

4444
private final Set<ConfigurationPropertyName> boundNames = new HashSet<>();
4545

46+
private final Set<ConfigurationPropertyName> attemptedNames = new HashSet<>();
47+
4648
private final Function<ConfigurationPropertySource, Boolean> filter;
4749

4850
NoUnboundElementsBindHandler() {
@@ -58,6 +60,12 @@ public NoUnboundElementsBindHandler(BindHandler parent, Function<ConfigurationPr
5860
this.filter = filter;
5961
}
6062

63+
@Override
64+
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
65+
this.attemptedNames.add(name);
66+
return super.onStart(name, target, context);
67+
}
68+
6169
@Override
6270
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
6371
this.boundNames.add(name);
@@ -108,11 +116,54 @@ private boolean isUnbound(ConfigurationPropertyName name, ConfigurationPropertyN
108116

109117
private boolean isOverriddenCollectionElement(ConfigurationPropertyName candidate) {
110118
int lastIndex = candidate.getNumberOfElements() - 1;
111-
if (candidate.isNumericIndex(lastIndex)) {
119+
if (candidate.isLastElementIndexed()) {
112120
ConfigurationPropertyName propertyName = candidate.chop(lastIndex);
113121
return this.boundNames.contains(propertyName);
114122
}
123+
Indexed indexed = getIndexed(candidate);
124+
if (indexed != null) {
125+
String zeroethProperty = indexed.getName() + "[0]";
126+
if (this.boundNames.contains(ConfigurationPropertyName.of(zeroethProperty))) {
127+
String nestedZeroethProperty = zeroethProperty + "." + indexed.getNestedPropertyName();
128+
return isCandidateValidPropertyName(nestedZeroethProperty);
129+
}
130+
}
115131
return false;
116132
}
117133

134+
private boolean isCandidateValidPropertyName(String nestedZeroethProperty) {
135+
return this.attemptedNames.contains(ConfigurationPropertyName.of(nestedZeroethProperty));
136+
}
137+
138+
private Indexed getIndexed(ConfigurationPropertyName candidate) {
139+
for (int i = 0; i < candidate.getNumberOfElements(); i++) {
140+
if (candidate.isNumericIndex(i)) {
141+
return new Indexed(candidate.chop(i).toString(),
142+
candidate.getElement(i + 1, ConfigurationPropertyName.Form.UNIFORM));
143+
}
144+
}
145+
return null;
146+
}
147+
148+
private static final class Indexed {
149+
150+
private final String name;
151+
152+
private final String nestedPropertyName;
153+
154+
private Indexed(String name, String nestedPropertyName) {
155+
this.name = name;
156+
this.nestedPropertyName = nestedPropertyName;
157+
}
158+
159+
public String getName() {
160+
return this.name;
161+
}
162+
163+
public String getNestedPropertyName() {
164+
return this.nestedPropertyName;
165+
}
166+
167+
}
168+
118169
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,42 @@ public void bindWhenUsingNoUnboundElementsHandlerAndUnboundListElementsShouldThr
131131
.contains("The elements [example.foo[0]] were left unbound"));
132132
}
133133

134+
@Test
135+
public void bindWhenUsingNoUnboundElementsHandlerShouldBindIfUnboundNestedCollectionProperties() {
136+
MockConfigurationPropertySource source1 = new MockConfigurationPropertySource();
137+
source1.put("example.nested[0].string-value", "bar");
138+
MockConfigurationPropertySource source2 = new MockConfigurationPropertySource();
139+
source2.put("example.nested[0].string-value", "bar");
140+
source2.put("example.nested[0].int-value", "2");
141+
source2.put("example.nested[1].string-value", "baz");
142+
source2.put("example.nested[1].other-nested.baz", "baz");
143+
this.sources.add(source1);
144+
this.sources.add(source2);
145+
this.binder = new Binder(this.sources);
146+
NoUnboundElementsBindHandler handler = new NoUnboundElementsBindHandler();
147+
ExampleWithNestedList bound = this.binder.bind("example", Bindable.of(ExampleWithNestedList.class), handler)
148+
.get();
149+
assertThat(bound.getNested().get(0).getStringValue()).isEqualTo("bar");
150+
}
151+
152+
@Test
153+
public void bindWhenUsingNoUnboundElementsHandlerAndUnboundCollectionElementsWithInvalidPropertyShouldThrowException() {
154+
MockConfigurationPropertySource source1 = new MockConfigurationPropertySource();
155+
source1.put("example.nested[0].string-value", "bar");
156+
MockConfigurationPropertySource source2 = new MockConfigurationPropertySource();
157+
source2.put("example.nested[0].string-value", "bar");
158+
source2.put("example.nested[1].int-value", "1");
159+
source2.put("example.nested[1].invalid", "baz");
160+
this.sources.add(source1);
161+
this.sources.add(source2);
162+
this.binder = new Binder(this.sources);
163+
assertThatExceptionOfType(BindException.class)
164+
.isThrownBy(() -> this.binder.bind("example", Bindable.of(ExampleWithNestedList.class),
165+
new NoUnboundElementsBindHandler()))
166+
.satisfies((ex) -> assertThat(ex.getCause().getMessage())
167+
.contains("The elements [example.nested[1].invalid] were left unbound"));
168+
}
169+
134170
public static class Example {
135171

136172
private String foo;
@@ -159,4 +195,66 @@ public void setFoo(List<String> foo) {
159195

160196
}
161197

198+
public static class ExampleWithNestedList {
199+
200+
private List<Nested> nested;
201+
202+
public List<Nested> getNested() {
203+
return this.nested;
204+
}
205+
206+
public void setNested(List<Nested> nested) {
207+
this.nested = nested;
208+
}
209+
210+
}
211+
212+
static class Nested {
213+
214+
private String stringValue;
215+
216+
private Integer intValue;
217+
218+
private OtherNested otherNested;
219+
220+
public String getStringValue() {
221+
return this.stringValue;
222+
}
223+
224+
public void setStringValue(String value) {
225+
this.stringValue = value;
226+
}
227+
228+
public Integer getIntValue() {
229+
return this.intValue;
230+
}
231+
232+
public void setIntValue(Integer intValue) {
233+
this.intValue = intValue;
234+
}
235+
236+
public OtherNested getOtherNested() {
237+
return this.otherNested;
238+
}
239+
240+
public void setOtherNested(OtherNested otherNested) {
241+
this.otherNested = otherNested;
242+
}
243+
244+
}
245+
246+
static class OtherNested {
247+
248+
private String baz;
249+
250+
public String getBaz() {
251+
return this.baz;
252+
}
253+
254+
public void setBaz(String baz) {
255+
this.baz = baz;
256+
}
257+
258+
}
259+
162260
}

0 commit comments

Comments
 (0)