From 91c1fc3d97dfca0170752eaddca0d8d956905f41 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 25 Mar 2019 15:49:37 -0700 Subject: [PATCH] Ignore unbound collection properties if collection bound As of Spring Boot 2.0, if Collection properties are specified in multiple property sources, only the elements from the property source with the highest precedence are used for binding. This caused an `UnboundConfigurationPropertiesException` if the size of the collection from the higher order property source was smaller and `ignoreUnknownFields` was set to true. This commit ignores unbound collection properties if the collection was properly bound. Fixes gh-16290 --- .../handler/NoUnboundElementsBindHandler.java | 18 +++++++- .../NoUnboundElementsBindHandlerTests.java | 43 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) 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 4f1ab5e123b..6f175a84658 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 @@ -106,7 +106,23 @@ public class NoUnboundElementsBindHandler extends AbstractBindHandler { private boolean isUnbound(ConfigurationPropertyName name, ConfigurationPropertyName candidate) { - return name.isAncestorOf(candidate) && !this.boundNames.contains(candidate); + if (name.isAncestorOf(candidate)) { + if (!this.boundNames.contains(candidate) + && !isOverriddenCollectionElement(candidate)) { + return true; + } + } + return false; + } + + private boolean isOverriddenCollectionElement(ConfigurationPropertyName candidate) { + int length = candidate.getNumberOfElements(); + if (candidate.isNumericIndex(length - 1)) { + ConfigurationPropertyName propertyName = candidate + .chop(candidate.getNumberOfElements() - 1); + return this.boundNames.contains(propertyName); + } + return false; } } 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 93bf00bf06f..2d71ff56215 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 @@ -108,6 +108,35 @@ public class NoUnboundElementsBindHandlerTests { assertThat(bound.getFoo()).isEqualTo("bar"); } + @Test + public void bindWhenUsingNoUnboundElementsHandlerShouldBindIfUnboundCollectionProperties() { + MockConfigurationPropertySource source1 = new MockConfigurationPropertySource(); + source1.put("example.foo[0]", "bar"); + MockConfigurationPropertySource source2 = new MockConfigurationPropertySource(); + source2.put("example.foo[0]", "bar"); + source2.put("example.foo[1]", "baz"); + this.sources.add(source1); + this.sources.add(source2); + this.binder = new Binder(this.sources); + NoUnboundElementsBindHandler handler = new NoUnboundElementsBindHandler(); + ExampleWithList bound = this.binder + .bind("example", Bindable.of(ExampleWithList.class), handler).get(); + assertThat(bound.getFoo()).containsExactly("bar"); + } + + @Test + public void bindWhenUsingNoUnboundElementsHandlerAndUnboundListElementsShouldThrowException() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("example.foo[0]", "bar"); + this.sources.add(source); + this.binder = new Binder(this.sources); + assertThatExceptionOfType(BindException.class) + .isThrownBy(() -> this.binder.bind("example", Bindable.of(Example.class), + new NoUnboundElementsBindHandler())) + .satisfies((ex) -> assertThat(ex.getCause().getMessage()) + .contains("The elements [example.foo[0]] were left unbound")); + } + public static class Example { private String foo; @@ -122,4 +151,18 @@ public class NoUnboundElementsBindHandlerTests { } + public static class ExampleWithList { + + private List foo; + + public List getFoo() { + return this.foo; + } + + public void setFoo(List foo) { + this.foo = foo; + } + + } + }