diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/OriginCapablePropertyValue.java b/spring-boot/src/main/java/org/springframework/boot/bind/OriginCapablePropertyValue.java new file mode 100644 index 0000000000..cfdb9d9f1b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/bind/OriginCapablePropertyValue.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.bind; + +import org.springframework.beans.PropertyValue; +import org.springframework.core.env.PropertySource; + +/** + * A {@link PropertyValue} that can provide information about its origin. + * + * @author Andy Wilkinson + */ +class OriginCapablePropertyValue extends PropertyValue { + + private static final String ATTRIBUTE_PROPERTY_ORIGIN = "propertyOrigin"; + + private final PropertyOrigin origin; + + public OriginCapablePropertyValue(PropertyValue propertyValue) { + this(propertyValue.getName(), propertyValue.getValue(), + (PropertyOrigin) propertyValue.getAttribute(ATTRIBUTE_PROPERTY_ORIGIN)); + } + + public OriginCapablePropertyValue(String name, Object value, String originName, + PropertySource originSource) { + this(name, value, new PropertyOrigin(originSource, originName)); + } + + public OriginCapablePropertyValue(String name, Object value, PropertyOrigin origin) { + super(name, value); + this.origin = origin; + setAttribute(ATTRIBUTE_PROPERTY_ORIGIN, origin); + } + + public PropertyOrigin getOrigin() { + return this.origin; + } + + @Override + public String toString() { + String name = this.origin != null ? this.origin.getName() : this.getName(); + String source = this.origin.getSource() != null ? this.origin.getSource() + .getName() : "unknown"; + return "'" + name + "' from '" + source + "'"; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/PropertyOrigin.java b/spring-boot/src/main/java/org/springframework/boot/bind/PropertyOrigin.java new file mode 100644 index 0000000000..fa39d62714 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/bind/PropertyOrigin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.bind; + +import org.springframework.core.env.PropertySource; + +/** + * The origin of a property, specifically its source and its name before any prefix was + * removed. + * + * @author Andy Wilkinson + * @since 1.3.0 + */ +public class PropertyOrigin { + + private final PropertySource source; + + private final String name; + + PropertyOrigin(PropertySource source, String name) { + this.name = name; + this.source = source; + } + + public PropertySource getSource() { + return this.source; + } + + public String getName() { + return this.name; + } + +} \ No newline at end of file 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 cd817f29ab..1c14719d4e 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 @@ -120,7 +120,7 @@ public class PropertySourcesPropertyValues implements PropertyValues { for (String propertyName : source.getPropertyNames()) { if (includes.matches(propertyName)) { Object value = getEnumerableProperty(source, resolver, propertyName); - putIfAbsent(propertyName, value); + putIfAbsent(propertyName, value, source); } } } @@ -155,13 +155,14 @@ public class PropertySourcesPropertyValues implements PropertyValues { if (value == null) { value = source.getProperty(propertyName.toUpperCase()); } - putIfAbsent(propertyName, value); + putIfAbsent(propertyName, value, source); } } - private void putIfAbsent(String propertyName, Object value) { + private void putIfAbsent(String propertyName, Object value, PropertySource source) { if (value != null && !this.propertyValues.containsKey(propertyName)) { - this.propertyValues.put(propertyName, new PropertyValue(propertyName, value)); + this.propertyValues.put(propertyName, new OriginCapablePropertyValue( + propertyName, value, propertyName, source)); } } @@ -180,7 +181,8 @@ public class PropertySourcesPropertyValues implements PropertyValues { for (PropertySource source : this.propertySources) { Object value = source.getProperty(propertyName); if (value != null) { - propertyValue = new PropertyValue(propertyName, value); + propertyValue = new OriginCapablePropertyValue(propertyName, value, + propertyName, source); this.propertyValues.put(propertyName, propertyValue); return propertyValue; } diff --git a/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedBindingNotWritablePropertyException.java b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedBindingNotWritablePropertyException.java new file mode 100644 index 0000000000..e6249bba56 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/bind/RelaxedBindingNotWritablePropertyException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.bind; + +import org.springframework.beans.NotWritablePropertyException; + +/** + * A custom {@link NotWritablePropertyException} that is thrown when a failure occurs + * during relaxed binding + * + * @see RelaxedDataBinder + * @author Andy Wilkinson + * @since 1.3.0 + */ +public class RelaxedBindingNotWritablePropertyException extends + NotWritablePropertyException { + + private final String message; + + private final PropertyOrigin propertyOrigin; + + RelaxedBindingNotWritablePropertyException(NotWritablePropertyException ex, + PropertyOrigin propertyOrigin) { + super(ex.getBeanClass(), ex.getPropertyName()); + this.propertyOrigin = propertyOrigin; + this.message = "Failed to bind '" + propertyOrigin.getName() + "' from '" + + propertyOrigin.getSource().getName() + "' to '" + ex.getPropertyName() + + "' property on '" + ex.getBeanClass().getName() + "'"; + } + + @Override + public String getMessage() { + return this.message; + } + + public PropertyOrigin getPropertyOrigin() { + return this.propertyOrigin; + } + +} 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 153aad6a18..cc2c9945a3 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 @@ -29,13 +29,17 @@ import java.util.Set; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.BeansException; import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.NotWritablePropertyException; import org.springframework.beans.PropertyValue; import org.springframework.core.convert.TypeDescriptor; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import org.springframework.validation.AbstractPropertyBindingResult; +import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.DataBinder; /** @@ -115,19 +119,8 @@ public class RelaxedDataBinder extends DataBinder { return this; } - @Override - public void initBeanPropertyAccess() { - super.initBeanPropertyAccess(); - // Hook in the RelaxedConversionService - getInternalBindingResult().initConversion( - new RelaxedConversionService(getConversionService())); - } - @Override protected void doBind(MutablePropertyValues propertyValues) { - // Harmless additional property editor comes in very handy sometimes... - getPropertyEditorRegistry().registerCustomEditor(InetAddress.class, - new InetAddressEditor()); super.doBind(modifyProperties(propertyValues, getTarget())); } @@ -213,7 +206,9 @@ public class RelaxedDataBinder extends DataBinder { if (name.startsWith(candidate)) { name = name.substring(candidate.length()); if (!(this.ignoreNestedProperties && name.contains("."))) { - rtn.add(name, value.getValue()); + PropertyOrigin propertyOrigin = findPropertyOrigin(value); + rtn.addPropertyValue(new OriginCapablePropertyValue(name, value + .getValue(), propertyOrigin)); } } } @@ -245,6 +240,44 @@ public class RelaxedDataBinder extends DataBinder { return initializePath(wrapper, new BeanPath(path), 0); } + @Override + protected AbstractPropertyBindingResult createBeanPropertyBindingResult() { + return new BeanPropertyBindingResult(getTarget(), getObjectName(), + isAutoGrowNestedPaths(), getAutoGrowCollectionLimit()) { + @Override + protected BeanWrapper createBeanWrapper() { + BeanWrapper beanWrapper = new BeanWrapperImpl(getTarget()) { + @Override + public void setPropertyValue(PropertyValue pv) throws BeansException { + try { + super.setPropertyValue(pv); + } + catch (NotWritablePropertyException ex) { + PropertyOrigin origin = findPropertyOrigin(pv); + if (origin != null) { + throw new RelaxedBindingNotWritablePropertyException(ex, + origin); + } + throw ex; + } + } + }; + beanWrapper.setConversionService(new RelaxedConversionService( + getConversionService())); + beanWrapper.registerCustomEditor(InetAddress.class, + new InetAddressEditor()); + return beanWrapper; + } + }; + } + + private PropertyOrigin findPropertyOrigin(PropertyValue propertyValue) { + if (propertyValue instanceof OriginCapablePropertyValue) { + return ((OriginCapablePropertyValue) propertyValue).getOrigin(); + } + return new OriginCapablePropertyValue(propertyValue).getOrigin(); + } + private String initializePath(BeanWrapper wrapper, BeanPath path, int index) { String prefix = path.prefix(index); String key = path.name(index); 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 2fc96302dd..114d698cbe 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 @@ -309,7 +309,7 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc return ""; } StringBuilder details = new StringBuilder(); - details.append("target=").append( + details.append("prefix=").append( (StringUtils.hasLength(annotation.value()) ? annotation.value() : annotation.prefix())); details.append(", ignoreInvalidFields=").append(annotation.ignoreInvalidFields()); diff --git a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java index 0a10edb001..25bcf41edf 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.boot.bind.RelaxedBindingNotWritablePropertyException; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -42,6 +43,7 @@ import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -85,6 +87,23 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } } + @Test + public void unknonwFieldFailureMessageContainsDetailsOfPropertyOrigin() { + this.context = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, "com.example.baz:spam"); + this.context.register(TestConfiguration.class); + try { + this.context.refresh(); + fail("Expected exception"); + } + catch (BeanCreationException ex) { + RelaxedBindingNotWritablePropertyException bex = (RelaxedBindingNotWritablePropertyException) ex.getRootCause(); + assertThat(bex.getMessage(), + startsWith("Failed to bind 'com.example.baz' from 'test' to 'baz' " + + "property on '" + TestConfiguration.class.getName())); + } + } + @Test public void testValidationWithoutJSR303() { this.context = new AnnotationConfigApplicationContext(); @@ -402,6 +421,23 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } + @Configuration + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "com.example", ignoreUnknownFields = false) + public static class TestConfiguration { + + private String bar; + + public void setBar(String bar) { + this.bar = bar; + } + + public String getBar() { + return this.bar; + } + + } + @ConfigurationProperties(prefix = "test") public static class PropertyWithJSR303 extends PropertyWithoutJSR303 {