diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java index fef84878b9..d61e8a4276 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertiesConfigurationFactory.java @@ -67,6 +67,8 @@ public class PropertiesConfigurationFactory implements FactoryBean, private boolean hasBeenBound = false; + private boolean ignoreNestedProperties = false; + private String targetName; private ConversionService conversionService; @@ -90,6 +92,17 @@ public class PropertiesConfigurationFactory implements FactoryBean, this.target = (T) BeanUtils.instantiate(type); } + /** + * Flag to disable binding of nested properties (i.e. those with period separators in + * their paths). Can be useful to disable this if the name prefix is empty and you + * don't want to ignore unknown fields. + * + * @param ignoreNestedProperties the flag to set (default false) + */ + public void setIgnoreNestedProperties(boolean ignoreNestedProperties) { + this.ignoreNestedProperties = ignoreNestedProperties; + } + /** * Set whether to ignore unknown fields, that is, whether to ignore bind parameters * that do not have corresponding fields in the target object. @@ -222,6 +235,7 @@ public class PropertiesConfigurationFactory implements FactoryBean, if (this.conversionService != null) { dataBinder.setConversionService(this.conversionService); } + dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties); dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields); dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields); customizeBinder(dataBinder); diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java index 831326accb..bd2016268c 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertySourcesPropertyValues.java @@ -16,6 +16,7 @@ package org.springframework.boot.bind; +import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,6 +28,7 @@ import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.env.PropertySourcesPropertyResolver; +import org.springframework.core.env.StandardEnvironment; import org.springframework.validation.DataBinder; /** @@ -48,10 +50,14 @@ public class PropertySourcesPropertyValues implements PropertyValues { */ public PropertySourcesPropertyValues(PropertySources propertySources) { this.propertySources = propertySources; + Collection nonEnumerables = Arrays.asList( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver( propertySources); for (PropertySource source : propertySources) { - if (source instanceof EnumerablePropertySource) { + if (source instanceof EnumerablePropertySource + && !nonEnumerables.contains(source.getName())) { EnumerablePropertySource enumerable = (EnumerablePropertySource) source; if (enumerable.getPropertyNames().length > 0) { for (String propertyName : enumerable.getPropertyNames()) { diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java index 3f63ba07fe..04c4c9cf46 100644 --- a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java @@ -43,6 +43,8 @@ public class RelaxedDataBinder extends DataBinder { private String namePrefix; + private boolean ignoreNestedProperties = false; + /** * @param target the target into which properties are bound */ @@ -69,6 +71,17 @@ public class RelaxedDataBinder extends DataBinder { this.namePrefix = (StringUtils.hasLength(namePrefix) ? namePrefix + "." : null); } + /** + * Flag to disable binding of nested properties (i.e. those with period separators in + * their paths). Can be useful to disable this if the name prefix is empty and you + * don't want to ignore unknown fields. + * + * @param ignoreNestedProperties the flag to set (default false) + */ + public void setIgnoreNestedProperties(boolean ignoreNestedProperties) { + this.ignoreNestedProperties = ignoreNestedProperties; + } + @Override protected void doBind(MutablePropertyValues propertyValues) { propertyValues = modifyProperties(propertyValues, getTarget()); @@ -115,18 +128,28 @@ public class RelaxedDataBinder extends DataBinder { private MutablePropertyValues getProperyValuesForNamePrefix( MutablePropertyValues propertyValues) { - if (this.namePrefix == null) { + if (!StringUtils.hasText(this.namePrefix) && !this.ignoreNestedProperties) { return propertyValues; } + int prefixLength = StringUtils.hasText(this.namePrefix) ? this.namePrefix + .length() : 0; MutablePropertyValues rtn = new MutablePropertyValues(); for (PropertyValue pv : propertyValues.getPropertyValues()) { String name = pv.getName(); - for (String candidate : new RelaxedNames(this.namePrefix)) { - if (name.startsWith(candidate)) { - name = name.substring(candidate.length()); + if (this.ignoreNestedProperties) { + name = name.substring(prefixLength); + if (!name.contains(".")) { rtn.add(name, pv.getValue()); } } + else { + for (String candidate : new RelaxedNames(this.namePrefix)) { + if (name.startsWith(candidate)) { + name = name.substring(candidate.length()); + rtn.add(name, pv.getValue()); + } + } + } } return rtn; } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java index 5a544b3af8..681245c58f 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java @@ -58,6 +58,13 @@ public @interface ConfigurationProperties { */ boolean ignoreInvalidFields() default false; + /** + * Flag to indicate that when binding to this object fields with periods in their + * names should be ignored. + * @return the flag value (default false) + */ + boolean ignoreNestedProperties() default false; + /** * Flag to indicate that when binding to this object unknown fields should be ignored. * An unknown field could be a sign of a mistake in the Properties. diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index d2cf38097e..f23d0489a0 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -302,6 +302,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc if (annotation != null) { factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields()); factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields()); + factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties()); String targetName = (StringUtils.hasLength(annotation.value()) ? annotation .value() : annotation.name()); if (StringUtils.hasLength(targetName)) { diff --git a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java index 8094ffc479..f9e2b7109f 100644 --- a/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java @@ -312,6 +312,32 @@ public class RelaxedDataBinderTests { assertEquals(123, target.getValue()); } + @Test + public void testOnlyTopLevelFields() throws Exception { + VanillaTarget target = new VanillaTarget(); + RelaxedDataBinder binder = getBinder(target, null); + binder.setIgnoreUnknownFields(false); + binder.setIgnoreNestedProperties(true); + BindingResult result = bind(binder, target, "foo: bar\n" + "value: 123\n" + + "nested.bar: spam"); + assertEquals(123, target.getValue()); + assertEquals("bar", target.getFoo()); + assertEquals(0, result.getErrorCount()); + } + + @Test + public void testNoNestedFields() throws Exception { + VanillaTarget target = new VanillaTarget(); + RelaxedDataBinder binder = getBinder(target, "foo"); + binder.setIgnoreUnknownFields(false); + binder.setIgnoreNestedProperties(true); + BindingResult result = bind(binder, target, "foo.foo: bar\n" + "foo.value: 123\n" + + "foo.nested.bar: spam"); + assertEquals(123, target.getValue()); + assertEquals("bar", target.getFoo()); + assertEquals(0, result.getErrorCount()); + } + @Test public void testBindMap() throws Exception { Map target = new LinkedHashMap(); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java index b4dd4a7101..29e17699f2 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/EnableConfigurationPropertiesTests.java @@ -52,6 +52,26 @@ public class EnableConfigurationPropertiesTests { assertEquals("foo", this.context.getBean(TestProperties.class).name); } + @Test + public void testStrictPropertiesBinding() { + this.context.register(StrictTestConfiguration.class); + TestUtils.addEnviroment(this.context, "name:foo"); + this.context.refresh(); + assertEquals(1, + this.context.getBeanNamesForType(StrictTestProperties.class).length); + assertEquals("foo", this.context.getBean(TestProperties.class).name); + } + + @Test + public void testIgnoreNestedPropertiesBinding() { + this.context.register(IgnoreNestedTestConfiguration.class); + TestUtils.addEnviroment(this.context, "name:foo", "nested.name:bar"); + this.context.refresh(); + assertEquals(1, + this.context.getBeanNamesForType(IgnoreNestedTestProperties.class).length); + assertEquals("foo", this.context.getBean(TestProperties.class).name); + } + @Test public void testNestedPropertiesBinding() { this.context.register(NestedConfiguration.class); @@ -195,6 +215,16 @@ public class EnableConfigurationPropertiesTests { protected static class TestConfiguration { } + @Configuration + @EnableConfigurationProperties(StrictTestProperties.class) + protected static class StrictTestConfiguration { + } + + @Configuration + @EnableConfigurationProperties(IgnoreNestedTestProperties.class) + protected static class IgnoreNestedTestConfiguration { + } + @Configuration @EnableConfigurationProperties(DerivedProperties.class) protected static class DerivedConfiguration { @@ -300,6 +330,16 @@ public class EnableConfigurationPropertiesTests { } } + @ConfigurationProperties(ignoreUnknownFields = false) + protected static class StrictTestProperties extends TestProperties { + + } + + @ConfigurationProperties(ignoreUnknownFields = false, ignoreNestedProperties = true) + protected static class IgnoreNestedTestProperties extends TestProperties { + + } + protected static class MoreProperties { private String name;