/* * Copyright 2012-2017 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.context.properties.bind; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; import org.springframework.format.annotation.DateTimeFormat; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link JavaBeanBinder}. * * @author Phillip Webb * @author Madhura Bhave */ public class JavaBeanBinderTests { @Rule public ExpectedException thrown = ExpectedException.none(); private List<ConfigurationPropertySource> sources = new ArrayList<>(); private Binder binder; @Before public void setup() { this.binder = new Binder(this.sources); } @Test public void bindToClassShouldCreateBoundBean() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.int-value", "12"); source.put("foo.long-value", "34"); source.put("foo.string-value", "foo"); source.put("foo.enum-value", "foo-bar"); this.sources.add(source); ExampleValueBean bean = this.binder .bind("foo", Bindable.of(ExampleValueBean.class)).get(); assertThat(bean.getIntValue()).isEqualTo(12); assertThat(bean.getLongValue()).isEqualTo(34); assertThat(bean.getStringValue()).isEqualTo("foo"); assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); } @Test public void bindToClassWhenHasNoPrefixShouldCreateBoundBean() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("int-value", "12"); source.put("long-value", "34"); source.put("string-value", "foo"); source.put("enum-value", "foo-bar"); this.sources.add(source); ExampleValueBean bean = this.binder.bind(ConfigurationPropertyName.of(""), Bindable.of(ExampleValueBean.class)).get(); assertThat(bean.getIntValue()).isEqualTo(12); assertThat(bean.getLongValue()).isEqualTo(34); assertThat(bean.getStringValue()).isEqualTo("foo"); assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); } @Test public void bindToInstanceShouldBindToInstance() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.int-value", "12"); source.put("foo.long-value", "34"); source.put("foo.string-value", "foo"); source.put("foo.enum-value", "foo-bar"); this.sources.add(source); ExampleValueBean bean = new ExampleValueBean(); ExampleValueBean boundBean = this.binder .bind("foo", Bindable.of(ExampleValueBean.class).withExistingValue(bean)) .get(); assertThat(boundBean).isSameAs(bean); assertThat(bean.getIntValue()).isEqualTo(12); assertThat(bean.getLongValue()).isEqualTo(34); assertThat(bean.getStringValue()).isEqualTo("foo"); assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); } @Test public void bindToInstanceWithNoPropertiesShouldReturnUnbound() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); this.sources.add(source); ExampleDefaultsBean bean = new ExampleDefaultsBean(); BindResult<ExampleDefaultsBean> boundBean = this.binder.bind("foo", Bindable.of(ExampleDefaultsBean.class).withExistingValue(bean)); assertThat(boundBean.isBound()).isFalse(); assertThat(bean.getFoo()).isEqualTo(123); assertThat(bean.getBar()).isEqualTo(456); } @Test public void bindToClassShouldLeaveDefaults() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.bar", "999"); this.sources.add(source); ExampleDefaultsBean bean = this.binder .bind("foo", Bindable.of(ExampleDefaultsBean.class)).get(); assertThat(bean.getFoo()).isEqualTo(123); assertThat(bean.getBar()).isEqualTo(999); } @Test public void bindToExistingInstanceShouldLeaveDefaults() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.bar", "999"); this.sources.add(source); ExampleDefaultsBean bean = new ExampleDefaultsBean(); bean.setFoo(888); ExampleDefaultsBean boundBean = this.binder .bind("foo", Bindable.of(ExampleDefaultsBean.class).withExistingValue(bean)) .get(); assertThat(boundBean).isSameAs(bean); assertThat(bean.getFoo()).isEqualTo(888); assertThat(bean.getBar()).isEqualTo(999); } @Test public void bindToClassShouldBindToMap() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.map.foo-bar", "1"); source.put("foo.map.bar-baz", "2"); this.sources.add(source); ExampleMapBean bean = this.binder.bind("foo", Bindable.of(ExampleMapBean.class)) .get(); assertThat(bean.getMap()).containsExactly(entry(ExampleEnum.FOO_BAR, 1), entry(ExampleEnum.BAR_BAZ, 2)); } @Test public void bindToClassShouldBindToList() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.list[0]", "foo-bar"); source.put("foo.list[1]", "bar-baz"); this.sources.add(source); ExampleListBean bean = this.binder.bind("foo", Bindable.of(ExampleListBean.class)) .get(); assertThat(bean.getList()).containsExactly(ExampleEnum.FOO_BAR, ExampleEnum.BAR_BAZ); } @Test public void bindToListIfUnboundElementsPresentShouldThrowException() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.list[0]", "foo-bar"); source.put("foo.list[2]", "bar-baz"); this.sources.add(source); this.thrown.expect(BindException.class); this.thrown.expectCause( Matchers.instanceOf(UnboundConfigurationPropertiesException.class)); this.binder.bind("foo", Bindable.of(ExampleListBean.class)); } @Test public void bindToClassShouldBindToSet() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.set[0]", "foo-bar"); source.put("foo.set[1]", "bar-baz"); this.sources.add(source); ExampleSetBean bean = this.binder.bind("foo", Bindable.of(ExampleSetBean.class)) .get(); assertThat(bean.getSet()).containsExactly(ExampleEnum.FOO_BAR, ExampleEnum.BAR_BAZ); } @Test public void bindToClassShouldBindToCollection() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.collection[0]", "foo-bar"); source.put("foo.collection[1]", "bar-baz"); this.sources.add(source); ExampleCollectionBean bean = this.binder .bind("foo", Bindable.of(ExampleCollectionBean.class)).get(); assertThat(bean.getCollection()).containsExactly(ExampleEnum.FOO_BAR, ExampleEnum.BAR_BAZ); } @Test public void bindToClassWhenHasNoSetterShouldBindToMap() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.map.foo-bar", "1"); source.put("foo.map.bar-baz", "2"); this.sources.add(source); ExampleMapBeanWithoutSetter bean = this.binder .bind("foo", Bindable.of(ExampleMapBeanWithoutSetter.class)).get(); assertThat(bean.getMap()).containsExactly(entry(ExampleEnum.FOO_BAR, 1), entry(ExampleEnum.BAR_BAZ, 2)); } @Test public void bindToClassWhenHasNoSetterShouldBindToList() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.list[0]", "foo-bar"); source.put("foo.list[1]", "bar-baz"); this.sources.add(source); ExampleListBeanWithoutSetter bean = this.binder .bind("foo", Bindable.of(ExampleListBeanWithoutSetter.class)).get(); assertThat(bean.getList()).containsExactly(ExampleEnum.FOO_BAR, ExampleEnum.BAR_BAZ); } @Test public void bindToClassWhenHasNoSetterShouldBindToSet() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.set[0]", "foo-bar"); source.put("foo.set[1]", "bar-baz"); this.sources.add(source); ExampleSetBeanWithoutSetter bean = this.binder .bind("foo", Bindable.of(ExampleSetBeanWithoutSetter.class)).get(); assertThat(bean.getSet()).containsExactly(ExampleEnum.FOO_BAR, ExampleEnum.BAR_BAZ); } @Test public void bindToClassWhenHasNoSetterShouldBindToCollection() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.collection[0]", "foo-bar"); source.put("foo.collection[1]", "bar-baz"); this.sources.add(source); ExampleCollectionBeanWithoutSetter bean = this.binder .bind("foo", Bindable.of(ExampleCollectionBeanWithoutSetter.class)).get(); assertThat(bean.getCollection()).containsExactly(ExampleEnum.FOO_BAR, ExampleEnum.BAR_BAZ); } @Test public void bindToClassShouldBindNested() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value-bean.int-value", "123"); source.put("foo.value-bean.string-value", "foo"); this.sources.add(source); ExampleNestedBean bean = this.binder .bind("foo", Bindable.of(ExampleNestedBean.class)).get(); assertThat(bean.getValueBean().getIntValue()).isEqualTo(123); assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); } @Test public void bindToClassWhenIterableShouldBindNestedBasedOnInstance() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value-bean.int-value", "123"); source.put("foo.value-bean.string-value", "foo"); this.sources.add(source); ExampleNestedBeanWithoutSetterOrType bean = this.binder .bind("foo", Bindable.of(ExampleNestedBeanWithoutSetterOrType.class)) .get(); ExampleValueBean valueBean = (ExampleValueBean) bean.getValueBean(); assertThat(valueBean.getIntValue()).isEqualTo(123); assertThat(valueBean.getStringValue()).isEqualTo("foo"); } @Test public void bindToClassWhenNotIterableShouldNotBindNestedBasedOnInstance() throws Exception { // If we can't tell that binding will happen, we don't want to randomly invoke // getters on the class and cause side effects MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value-bean.int-value", "123"); source.put("foo.value-bean.string-value", "foo"); this.sources.add(source.nonIterable()); BindResult<ExampleNestedBeanWithoutSetterOrType> bean = this.binder.bind("foo", Bindable.of(ExampleNestedBeanWithoutSetterOrType.class)); assertThat(bean.isBound()).isFalse(); } @Test public void bindToClassWhenHasNoSetterShouldBindNested() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value-bean.int-value", "123"); source.put("foo.value-bean.string-value", "foo"); this.sources.add(source); ExampleNestedBeanWithoutSetter bean = this.binder .bind("foo", Bindable.of(ExampleNestedBeanWithoutSetter.class)).get(); assertThat(bean.getValueBean().getIntValue()).isEqualTo(123); assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); } @Test public void bindToClassWhenHasNoSetterAndImmutableShouldThrowException() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.nested.foo", "bar"); this.sources.add(source); this.thrown.expect(BindException.class); this.binder.bind("foo", Bindable.of(ExampleImmutableNestedBeanWithoutSetter.class)); } @Test public void bindToInstanceWhenNoNestedShouldLeaveNestedAsNull() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("faf.value-bean.int-value", "123"); this.sources.add(source); ExampleNestedBean bean = new ExampleNestedBean(); BindResult<ExampleNestedBean> boundBean = this.binder.bind("foo", Bindable.of(ExampleNestedBean.class).withExistingValue(bean)); assertThat(boundBean.isBound()).isFalse(); assertThat(bean.getValueBean()).isNull(); } @Test public void bindToClassWhenPropertiesMissingShouldReturnUnbound() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("faf.int-value", "12"); this.sources.add(source); BindResult<ExampleValueBean> bean = this.binder.bind("foo", Bindable.of(ExampleValueBean.class)); assertThat(bean.isBound()).isFalse(); } @Test public void bindToClassWhenNoDefaultConstructorShouldReturnUnbound() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value", "bar"); this.sources.add(source); BindResult<ExampleWithNonDefaultConstructor> bean = this.binder.bind("foo", Bindable.of(ExampleWithNonDefaultConstructor.class)); assertThat(bean.isBound()).isFalse(); } @Test public void bindToInstanceWhenNoDefaultConstructorShouldBind() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value", "bar"); this.sources.add(source); ExampleWithNonDefaultConstructor bean = new ExampleWithNonDefaultConstructor( "faf"); ExampleWithNonDefaultConstructor boundBean = this.binder.bind("foo", Bindable .of(ExampleWithNonDefaultConstructor.class).withExistingValue(bean)) .get(); assertThat(boundBean).isSameAs(bean); assertThat(bean.getValue()).isEqualTo("bar"); } @Test public void bindToClassShouldBindHierarchy() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.int-value", "123"); source.put("foo.long-value", "456"); this.sources.add(source); ExampleSubclassBean bean = this.binder .bind("foo", Bindable.of(ExampleSubclassBean.class)).get(); assertThat(bean.getIntValue()).isEqualTo(123); assertThat(bean.getLongValue()).isEqualTo(456); } @Test public void bindToClassWhenPropertyCannotBeConvertedShouldThrowException() throws Exception { this.sources.add(new MockConfigurationPropertySource("foo.int-value", "foo")); this.thrown.expect(BindException.class); this.binder.bind("foo", Bindable.of(ExampleValueBean.class)); } @Test public void bindToClassWhenPropertyCannotBeConvertedAndIgnoreErrorsShouldNotSetValue() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.int-value", "12"); source.put("foo.long-value", "bang"); source.put("foo.string-value", "foo"); source.put("foo.enum-value", "foo-bar"); this.sources.add(source); IgnoreErrorsBindHandler handler = new IgnoreErrorsBindHandler(); ExampleValueBean bean = this.binder .bind("foo", Bindable.of(ExampleValueBean.class), handler).get(); assertThat(bean.getIntValue()).isEqualTo(12); assertThat(bean.getLongValue()).isEqualTo(0); assertThat(bean.getStringValue()).isEqualTo("foo"); assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); } @Test public void bindToClassWhenMismatchedGetSetShouldBind() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value", "123"); this.sources.add(source); ExampleMismatchBean bean = this.binder .bind("foo", Bindable.of(ExampleMismatchBean.class)).get(); assertThat(bean.getValue()).isEqualTo("123"); } @Test public void bindToClassShouldNotInvokeExtraMethods() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource( "foo.value", "123"); this.sources.add(source.nonIterable()); ExampleWithThrowingGetters bean = this.binder .bind("foo", Bindable.of(ExampleWithThrowingGetters.class)).get(); assertThat(bean.getValue()).isEqualTo(123); } @Test public void bindToClassWithSelfReferenceShouldBind() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value", "123"); this.sources.add(source); ExampleWithSelfReference bean = this.binder .bind("foo", Bindable.of(ExampleWithSelfReference.class)).get(); assertThat(bean.getValue()).isEqualTo(123); } @Test public void bindToInstanceWithExistingValueShouldReturnUnbound() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); this.sources.add(source); ExampleNestedBean existingValue = new ExampleNestedBean(); ExampleValueBean valueBean = new ExampleValueBean(); existingValue.setValueBean(valueBean); BindResult<ExampleNestedBean> result = this.binder.bind("foo", Bindable.of(ExampleNestedBean.class).withExistingValue(existingValue)); assertThat(result.isBound()).isFalse(); } @Test public void bindWithAnnotations() throws Exception { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.date", "2014-04-01"); this.sources.add(source); ConverterAnnotatedExampleBean bean = this.binder .bind("foo", Bindable.of(ConverterAnnotatedExampleBean.class)).get(); assertThat(bean.getDate().toString()).isEqualTo("2014-04-01"); } public static class ExampleValueBean { private int intValue; private long longValue; private String stringValue; private ExampleEnum enumValue; public int getIntValue() { return this.intValue; } public void setIntValue(int intValue) { this.intValue = intValue; } public long getLongValue() { return this.longValue; } public void setLongValue(long longValue) { this.longValue = longValue; } public String getStringValue() { return this.stringValue; } public void setStringValue(String stringValue) { this.stringValue = stringValue; } public ExampleEnum getEnumValue() { return this.enumValue; } public void setEnumValue(ExampleEnum enumValue) { this.enumValue = enumValue; } } public static class ExampleDefaultsBean { private int foo = 123; private int bar = 456; public int getFoo() { return this.foo; } public void setFoo(int foo) { this.foo = foo; } public int getBar() { return this.bar; } public void setBar(int bar) { this.bar = bar; } } public static class ExampleMapBean { private Map<ExampleEnum, Integer> map; public Map<ExampleEnum, Integer> getMap() { return this.map; } public void setMap(Map<ExampleEnum, Integer> map) { this.map = map; } } public static class ExampleListBean { private List<ExampleEnum> list; public List<ExampleEnum> getList() { return this.list; } public void setList(List<ExampleEnum> list) { this.list = list; } } public static class ExampleSetBean { private Set<ExampleEnum> set; public Set<ExampleEnum> getSet() { return this.set; } public void setSet(Set<ExampleEnum> set) { this.set = set; } } public static class ExampleCollectionBean { private Collection<ExampleEnum> collection; public Collection<ExampleEnum> getCollection() { return this.collection; } public void setCollection(Collection<ExampleEnum> collection) { this.collection = collection; } } public static class ExampleMapBeanWithoutSetter { private Map<ExampleEnum, Integer> map = new LinkedHashMap<>(); public Map<ExampleEnum, Integer> getMap() { return this.map; } } public static class ExampleListBeanWithoutSetter { private List<ExampleEnum> list = new ArrayList<>(); public List<ExampleEnum> getList() { return this.list; } } public static class ExampleSetBeanWithoutSetter { private Set<ExampleEnum> set = new LinkedHashSet<>(); public Set<ExampleEnum> getSet() { return this.set; } } public static class ExampleCollectionBeanWithoutSetter { private Collection<ExampleEnum> collection = new ArrayList<>(); public Collection<ExampleEnum> getCollection() { return this.collection; } } public static class ExampleNestedBean { private ExampleValueBean valueBean; public ExampleValueBean getValueBean() { return this.valueBean; } public void setValueBean(ExampleValueBean valueBean) { this.valueBean = valueBean; } } public static class ExampleNestedBeanWithoutSetter { private ExampleValueBean valueBean = new ExampleValueBean(); public ExampleValueBean getValueBean() { return this.valueBean; } } public static class ExampleNestedBeanWithoutSetterOrType { private ExampleValueBean valueBean = new ExampleValueBean(); public Object getValueBean() { return this.valueBean; } } public static class ExampleImmutableNestedBeanWithoutSetter { private NestedImmutable nested = new NestedImmutable(); public NestedImmutable getNested() { return this.nested; } public static class NestedImmutable { public String getFoo() { return "foo"; } } } public static class ExampleWithNonDefaultConstructor { private String value; public ExampleWithNonDefaultConstructor(String value) { this.value = value; } public String getValue() { return this.value; } public void setValue(String value) { this.value = value; } } public abstract static class ExampleSuperClassBean { private int intValue; public int getIntValue() { return this.intValue; } public void setIntValue(int intValue) { this.intValue = intValue; } } public static class ExampleSubclassBean extends ExampleSuperClassBean { private long longValue; public long getLongValue() { return this.longValue; } public void setLongValue(long longValue) { this.longValue = longValue; } } public static class ExampleMismatchBean { private int value; public String getValue() { return String.valueOf(this.value); } public void setValue(int value) { this.value = value; } } public static class ExampleWithThrowingGetters { private int value; public int getValue() { return this.value; } public void setValue(int value) { this.value = value; } public List<String> getNames() { throw new RuntimeException(); } public ExampleValueBean getNested() { throw new RuntimeException(); } } public static class ExampleWithSelfReference { private int value; private ExampleWithSelfReference self; public int getValue() { return this.value; } public void setValue(int value) { this.value = value; } public ExampleWithSelfReference getSelf() { return this.self; } public void setSelf(ExampleWithSelfReference self) { this.self = self; } } public enum ExampleEnum { FOO_BAR, BAR_BAZ } public static class ConverterAnnotatedExampleBean { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate date; public LocalDate getDate() { return this.date; } public void setDate(LocalDate date) { this.date = date; } } }