diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java index 754fce22bf..f15cf66e46 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandler.java @@ -43,6 +43,8 @@ public class NoUnboundElementsBindHandler extends AbstractBindHandler { private final Set boundNames = new HashSet<>(); + private final Set attemptedNames = new HashSet<>(); + private final Function filter; NoUnboundElementsBindHandler() { @@ -58,6 +60,12 @@ public class NoUnboundElementsBindHandler extends AbstractBindHandler { this.filter = filter; } + @Override + public Bindable onStart(ConfigurationPropertyName name, Bindable target, BindContext context) { + this.attemptedNames.add(name); + return super.onStart(name, target, context); + } + @Override public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { this.boundNames.add(name); @@ -108,11 +116,54 @@ public class NoUnboundElementsBindHandler extends AbstractBindHandler { private boolean isOverriddenCollectionElement(ConfigurationPropertyName candidate) { int lastIndex = candidate.getNumberOfElements() - 1; - if (candidate.isNumericIndex(lastIndex)) { + if (candidate.isLastElementIndexed()) { ConfigurationPropertyName propertyName = candidate.chop(lastIndex); return this.boundNames.contains(propertyName); } + Indexed indexed = getIndexed(candidate); + if (indexed != null) { + String zeroethProperty = indexed.getName() + "[0]"; + if (this.boundNames.contains(ConfigurationPropertyName.of(zeroethProperty))) { + String nestedZeroethProperty = zeroethProperty + "." + indexed.getNestedPropertyName(); + return isCandidateValidPropertyName(nestedZeroethProperty); + } + } return false; } + private boolean isCandidateValidPropertyName(String nestedZeroethProperty) { + return this.attemptedNames.contains(ConfigurationPropertyName.of(nestedZeroethProperty)); + } + + private Indexed getIndexed(ConfigurationPropertyName candidate) { + for (int i = 0; i < candidate.getNumberOfElements(); i++) { + if (candidate.isNumericIndex(i)) { + return new Indexed(candidate.chop(i).toString(), + candidate.getElement(i + 1, ConfigurationPropertyName.Form.UNIFORM)); + } + } + return null; + } + + private static final class Indexed { + + private final String name; + + private final String nestedPropertyName; + + private Indexed(String name, String nestedPropertyName) { + this.name = name; + this.nestedPropertyName = nestedPropertyName; + } + + public String getName() { + return this.name; + } + + public String getNestedPropertyName() { + return this.nestedPropertyName; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java index a6d84ceed1..4b3719221e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/handler/NoUnboundElementsBindHandlerTests.java @@ -131,6 +131,42 @@ public class NoUnboundElementsBindHandlerTests { .contains("The elements [example.foo[0]] were left unbound")); } + @Test + public void bindWhenUsingNoUnboundElementsHandlerShouldBindIfUnboundNestedCollectionProperties() { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("example.nested[0].string-value", "bar"); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("example.nested[0].string-value", "bar"); + source2.put("example.nested[0].int-value", "2"); + source2.put("example.nested[1].string-value", "baz"); + source2.put("example.nested[1].other-nested.baz", "baz"); + this.sources.add(source1); + this.sources.add(source2); + this.binder = new Binder(this.sources); + NoUnboundElementsBindHandler handler = new NoUnboundElementsBindHandler(); + ExampleWithNestedList bound = this.binder.bind("example", Bindable.of(ExampleWithNestedList.class), handler) + .get(); + assertThat(bound.getNested().get(0).getStringValue()).isEqualTo("bar"); + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerAndUnboundCollectionElementsWithInvalidPropertyShouldThrowException() { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("example.nested[0].string-value", "bar"); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("example.nested[0].string-value", "bar"); + source2.put("example.nested[1].int-value", "1"); + source2.put("example.nested[1].invalid", "baz"); + this.sources.add(source1); + this.sources.add(source2); + this.binder = new Binder(this.sources); + assertThatExceptionOfType(BindException.class) + .isThrownBy(() -> this.binder.bind("example", Bindable.of(ExampleWithNestedList.class), + new NoUnboundElementsBindHandler())) + .satisfies((ex) -> assertThat(ex.getCause().getMessage()) + .contains("The elements [example.nested[1].invalid] were left unbound")); + } + public static class Example { private String foo; @@ -159,4 +195,66 @@ public class NoUnboundElementsBindHandlerTests { } + public static class ExampleWithNestedList { + + private List nested; + + public List getNested() { + return this.nested; + } + + public void setNested(List nested) { + this.nested = nested; + } + + } + + static class Nested { + + private String stringValue; + + private Integer intValue; + + private OtherNested otherNested; + + public String getStringValue() { + return this.stringValue; + } + + public void setStringValue(String value) { + this.stringValue = value; + } + + public Integer getIntValue() { + return this.intValue; + } + + public void setIntValue(Integer intValue) { + this.intValue = intValue; + } + + public OtherNested getOtherNested() { + return this.otherNested; + } + + public void setOtherNested(OtherNested otherNested) { + this.otherNested = otherNested; + } + + } + + static class OtherNested { + + private String baz; + + public String getBaz() { + return this.baz; + } + + public void setBaz(String baz) { + this.baz = baz; + } + + } + }