From 7ca589d43c5a7354f86be56a5f50b2496124c358 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 6 Mar 2019 15:24:34 +0100 Subject: [PATCH] Add constructor based binding support See gh-8762 Co-authored-by: Stephane Nicoll --- .../ConfigurationPropertyDefaultValue.java | 43 +++ .../boot/context/properties/bind/Binder.java | 3 +- .../bind/ConstructorParametersBinder.java | 211 +++++++++++++ .../ConstructorParametersBinderTests.java | 286 ++++++++++++++++++ .../properties/bind/JavaBeanBinderTests.java | 6 +- .../KotlinConstructorParametersBinderTests.kt | 208 +++++++++++++ 6 files changed, 754 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyDefaultValue.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java create mode 100644 spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyDefaultValue.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyDefaultValue.java new file mode 100644 index 0000000000..c55d3e4e20 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertyDefaultValue.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2019 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that can be used to specify the default value when binding to an immutable + * property. + * + * @author Madhura Bhave + * @since 2.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.PARAMETER }) +@Documented +public @interface ConfigurationPropertyDefaultValue { + + /** + * The default value of the property. Can be an array of values for collection or + * array-based properties. + * @return the default value of the property. + */ + String[] value(); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 8f746fdd7d..50ca79dcb5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -62,6 +62,7 @@ public class Binder { static { List binders = new ArrayList<>(); + binders.add(new ConstructorParametersBinder()); binders.add(new JavaBeanBinder()); BEAN_BINDERS = Collections.unmodifiableList(binders); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java new file mode 100644 index 0000000000..2345832097 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java @@ -0,0 +1,211 @@ +/* + * Copyright 2012-2019 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.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.context.properties.ConfigurationPropertyDefaultValue; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.core.KotlinDetector; +import org.springframework.core.ResolvableType; + +/** + * {@link BeanBinder} for constructor based binding. + * + * @author Madhura Bhave + * @author Stephane Nicoll + */ +class ConstructorParametersBinder implements BeanBinder { + + private static boolean KOTLIN_PRESENT = KotlinDetector.isKotlinPresent(); + + private static final Map, Object> DEFAULT_TYPE_VALUES; + + static { + Map, Object> values = new HashMap<>(); + values.put(boolean.class, false); + values.put(byte.class, (byte) 0); + values.put(short.class, (short) 0); + values.put(int.class, 0); + values.put(long.class, (long) 0); + DEFAULT_TYPE_VALUES = Collections.unmodifiableMap(values); + } + + @Override + public T bind(ConfigurationPropertyName name, Bindable target, + Binder.Context context, BeanPropertyBinder propertyBinder) { + Bean bean = Bean.get(target); + if (bean == null) { + return null; + } + List bound = bind(propertyBinder, bean, context.getConverter()); + return (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray()); + } + + private List bind(BeanPropertyBinder propertyBinder, Bean bean, + BindConverter converter) { + List boundParams = new ArrayList<>(); + for (ConstructorParameter parameter : bean.getParameters().values()) { + Object bound = bind(parameter, propertyBinder); + if (bound == null) { + bound = getDefaultValue(parameter, bean, converter); + } + boundParams.add(bound); + } + return boundParams; + } + + private Object getDefaultValue(ConstructorParameter parameter, Bean bean, + BindConverter converter) { + if (parameter.getDefaultValue() != null) { + return converter.convert(parameter.getDefaultValue(), parameter.getType(), + parameter.getAnnotations()); + } + else { + Class resolve = parameter.getType().resolve(); + if (resolve != null && resolve.isPrimitive()) { + return (bean.kotlinType) ? null : DEFAULT_TYPE_VALUES.get(resolve); + } + } + return null; + } + + private Object bind(ConstructorParameter parameter, + BeanPropertyBinder propertyBinder) { + String propertyName = parameter.getName(); + ResolvableType type = parameter.getType(); + return propertyBinder.bindProperty(propertyName, Bindable.of(type)); + } + + private static final class Bean { + + private final boolean kotlinType; + + private final Constructor constructor; + + private final Map parameters; + + private Bean(boolean kotlinType, Constructor constructor, + Map parameters) { + this.kotlinType = kotlinType; + this.constructor = constructor; + this.parameters = parameters; + } + + public static Bean get(Bindable bindable) { + if (bindable.getValue() != null) { + return null; + } + Class type = bindable.getType().resolve(Object.class); + if (type.isEnum() || Modifier.isAbstract(type.getModifiers())) { + return null; + } + if (KOTLIN_PRESENT && KotlinDetector.isKotlinType(type)) { + Constructor primaryConstructor = BeanUtils + .findPrimaryConstructor(type); + if (primaryConstructor != null + && primaryConstructor.getParameterCount() > 0) { + return new Bean(true, primaryConstructor, + parseParameters(primaryConstructor)); + } + } + else { + Constructor[] constructors = type.getDeclaredConstructors(); + if (constructors.length == 1 && constructors[0].getParameterCount() > 0) { + Constructor constructor = constructors[0]; + return new Bean(false, constructor, parseParameters(constructor)); + } + } + return null; + } + + private static Map parseParameters( + Constructor constructor) { + Map parameters = new LinkedHashMap<>(); + for (Parameter parameter : constructor.getParameters()) { + String name = parameter.getName(); + ConfigurationPropertyDefaultValue[] annotationsByType = parameter + .getAnnotationsByType(ConfigurationPropertyDefaultValue.class); + String[] defaultValue = (annotationsByType.length > 0) + ? annotationsByType[0].value() : null; + parameters.computeIfAbsent(name, + (s) -> new ConstructorParameter(name, + ResolvableType.forClass(parameter.getType()), + parameter.getDeclaredAnnotations(), defaultValue)); + } + return parameters; + } + + public Map getParameters() { + return this.parameters; + } + + public Constructor getConstructor() { + return this.constructor; + } + + } + + /** + * A constructor parameter being bound. + */ + private static class ConstructorParameter { + + private final String name; + + private final ResolvableType type; + + private final Annotation[] annotations; + + private final String[] defaultValue; + + ConstructorParameter(String name, ResolvableType type, Annotation[] annotations, + String[] defaultValue) { + this.name = BeanPropertyName.toDashedForm(name); + this.type = type; + this.annotations = annotations; + this.defaultValue = defaultValue; + } + + public String getName() { + return this.name; + } + + public ResolvableType getType() { + return this.type; + } + + public Annotation[] getAnnotations() { + return this.annotations; + } + + public String[] getDefaultValue() { + return this.defaultValue; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java new file mode 100644 index 0000000000..c9feb993dd --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2012-2019 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.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.context.properties.ConfigurationPropertyDefaultValue; +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 static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConstructorParametersBinder}. + * + * @author Madhura Bhave + */ +public class ConstructorParametersBinderTests { + + private List sources = new ArrayList<>(); + + private Binder binder; + + @Before + public void setup() { + this.binder = new Binder(this.sources); + } + + @Test + public void bindToClassShouldCreateBoundBean() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + source.put("foo.long-value", "34"); + source.put("foo.boolean-value", "true"); + 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.isBooleanValue()).isTrue(); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToClassWhenHasNoPrefixShouldCreateBoundBean() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("int-value", "12"); + source.put("long-value", "34"); + source.put("boolean-value", "true"); + 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.isBooleanValue()).isTrue(); + assertThat(bean.getStringValue()).isEqualTo("foo"); + assertThat(bean.getEnumValue()).isEqualTo(ExampleEnum.FOO_BAR); + } + + @Test + public void bindToAbstractClassWithShouldNotBind() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.name", "test"); + this.sources.add(source); + boolean bound = this.binder.bind("foo", Bindable.of(ExampleAbstractBean.class)) + .isBound(); + assertThat(bound).isFalse(); + } + + @Test + public void bindToClassWithMultipleConstructorsShouldNotBind() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + this.sources.add(source); + boolean bound = this.binder + .bind("foo", Bindable.of(MultipleConstructorsBean.class)).isBound(); + assertThat(bound).isFalse(); + } + + @Test + public void bindToClassWithOnlyDefaultConstructorShouldNotBind() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.int-value", "12"); + this.sources.add(source); + boolean bound = this.binder.bind("foo", Bindable.of(DefaultConstructorBean.class)) + .isBound(); + assertThat(bound).isFalse(); + } + + @Test + public void bindToClassShouldBindNested() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.value-bean.int-value", "123"); + source.put("foo.value-bean.long-value", "34"); + 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().getLongValue()).isEqualTo(34); + assertThat(bean.getValueBean().isBooleanValue()).isFalse(); + assertThat(bean.getValueBean().getStringValue()).isEqualTo("foo"); + assertThat(bean.getValueBean().getEnumValue()).isNull(); + } + + @Test + public void bindToClassWithNoValueForPrimitiveShouldUseDefault() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.string-value", "foo"); + this.sources.add(source); + ExampleValueBean bean = this.binder + .bind("foo", Bindable.of(ExampleValueBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(0); + assertThat(bean.getLongValue()).isEqualTo(0); + assertThat(bean.isBooleanValue()).isEqualTo(false); + assertThat(bean.getStringValue()).isEqualTo("foo"); + } + + @Test + public void bindToClassWithNoValueAndDefaultValueShouldUseDefault() { + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.string-value", "foo"); + this.sources.add(source); + ExampleDefaultValueBean bean = this.binder + .bind("foo", Bindable.of(ExampleDefaultValueBean.class)).get(); + assertThat(bean.getIntValue()).isEqualTo(5); + assertThat(bean.getStringsList()).containsOnly("a", "b", "c"); + assertThat(bean.getCustomList()).containsOnly("x,y,z"); + } + + public static class ExampleValueBean { + + private final int intValue; + + private final long longValue; + + private final boolean booleanValue; + + private final String stringValue; + + private final ExampleEnum enumValue; + + public ExampleValueBean(int intValue, long longValue, boolean booleanValue, + String stringValue, ExampleEnum enumValue) { + this.intValue = intValue; + this.longValue = longValue; + this.booleanValue = booleanValue; + this.stringValue = stringValue; + this.enumValue = enumValue; + } + + public int getIntValue() { + return this.intValue; + } + + public long getLongValue() { + return this.longValue; + } + + public boolean isBooleanValue() { + return this.booleanValue; + } + + public String getStringValue() { + return this.stringValue; + } + + public ExampleEnum getEnumValue() { + return this.enumValue; + } + + } + + public enum ExampleEnum { + + FOO_BAR, + + BAR_BAZ + + } + + @SuppressWarnings("unused") + public static class MultipleConstructorsBean { + + public MultipleConstructorsBean(int intValue) { + this(intValue, 23L, "hello"); + } + + public MultipleConstructorsBean(int intValue, long longValue, + String stringValue) { + } + + } + + public abstract static class ExampleAbstractBean { + + private final String name; + + public ExampleAbstractBean(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + } + + public static class DefaultConstructorBean { + + public DefaultConstructorBean() { + + } + + } + + public static class ExampleNestedBean { + + private final ExampleValueBean valueBean; + + public ExampleNestedBean(ExampleValueBean valueBean) { + this.valueBean = valueBean; + } + + public ExampleValueBean getValueBean() { + return this.valueBean; + } + + } + + public static class ExampleDefaultValueBean { + + private final int intValue; + + private final List stringsList; + + private final List customList; + + public ExampleDefaultValueBean( + @ConfigurationPropertyDefaultValue("5") int intValue, + @ConfigurationPropertyDefaultValue({ "a", "b", + "c" }) List stringsList, + @ConfigurationPropertyDefaultValue("x,y,z") List customList) { + this.intValue = intValue; + this.stringsList = stringsList; + this.customList = customList; + } + + public int getIntValue() { + return this.intValue; + } + + public List getStringsList() { + return this.stringsList; + } + + public List getCustomList() { + return this.customList; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index 4a41f908da..dba5c910d8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -348,13 +348,15 @@ public class JavaBeanBinderTests { } @Test - public void bindToClassWhenNoDefaultConstructorShouldReturnUnbound() { + public void bindToClassWhenNoDefaultConstructorShouldBind() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.value", "bar"); this.sources.add(source); BindResult bean = this.binder.bind("foo", Bindable.of(ExampleWithNonDefaultConstructor.class)); - assertThat(bean.isBound()).isFalse(); + assertThat(bean.isBound()).isTrue(); + ExampleWithNonDefaultConstructor boundBean = bean.get(); + assertThat(boundBean.getValue()).isEqualTo("bar"); } @Test diff --git a/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt new file mode 100644 index 0000000000..9fc4a420ff --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/kotlin/org/springframework/boot/context/properties/bind/KotlinConstructorParametersBinderTests.kt @@ -0,0 +1,208 @@ +package org.springframework.boot.context.properties.bind + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.springframework.boot.context.properties.source.ConfigurationPropertyName +import org.springframework.boot.context.properties.source.MockConfigurationPropertySource + +/** + * Tests for `ConstructorParametersBinder`. + * + * @author Stephane Nicoll + */ +class KotlinConstructorParametersBinderTests { + + @Test + fun `Bind to class should create bound bean`() { + val source = MockConfigurationPropertySource() + source.put("foo.int-value", "12") + source.put("foo.long-value", "34") + source.put("foo.boolean-value", "true") + source.put("foo.string-value", "foo") + source.put("foo.enum-value", "foo-bar") + val binder = Binder(source) + val bean = binder.bind("foo", Bindable.of(ExampleValueBean::class.java)).get() + assertThat(bean.intValue).isEqualTo(12) + assertThat(bean.longValue).isEqualTo(34) + assertThat(bean.booleanValue).isTrue() + assertThat(bean.stringValue).isEqualTo("foo") + assertThat(bean.enumValue).isEqualTo(ExampleEnum.FOO_BAR) + } + + @Test + fun `Bind to class when has no prefix should create bound bean`() { + val source = MockConfigurationPropertySource() + source.put("int-value", "12") + source.put("long-value", "34") + source.put("boolean-value", "true") + source.put("string-value", "foo") + source.put("enum-value", "foo-bar") + val binder = Binder(source) + val bean = binder.bind(ConfigurationPropertyName.of(""), + Bindable.of(ExampleValueBean::class.java)).get() + assertThat(bean.intValue).isEqualTo(12) + assertThat(bean.longValue).isEqualTo(34) + assertThat(bean.booleanValue).isTrue() + assertThat(bean.stringValue).isEqualTo("foo") + assertThat(bean.enumValue).isEqualTo(ExampleEnum.FOO_BAR) + } + + @Test + fun `Bind to data class should create bound bean`() { + val source = MockConfigurationPropertySource() + source.put("foo.int-value", "12") + source.put("foo.long-value", "34") + source.put("foo.boolean-value", "true") + source.put("foo.string-value", "foo") + source.put("foo.enum-value", "foo-bar") + val binder = Binder(source) + val bean = binder.bind("foo", Bindable.of(ExampleDataClassBean::class.java)).get() + assertThat(bean.intValue).isEqualTo(12) + assertThat(bean.longValue).isEqualTo(34) + assertThat(bean.booleanValue).isTrue() + assertThat(bean.stringValue).isEqualTo("foo") + assertThat(bean.enumValue).isEqualTo(ExampleEnum.FOO_BAR) + } + + @Test + fun `Bind to class with multiple constructors and primary constructor should bind`() { + val source = MockConfigurationPropertySource() + source.put("foo.int-value", "12") + val binder = Binder(source) + val bindable = binder.bind("foo", Bindable.of( + MultipleConstructorsWithPrimaryConstructorBean::class.java)) + assertThat(bindable.isBound).isTrue() + assertThat(bindable.get().intValue).isEqualTo(12) + } + + @Test + fun `Bind to class with multiple constructors should not bind`() { + val source = MockConfigurationPropertySource() + source.put("foo.int-value", "12") + val binder = Binder(source) + val bindable = binder.bind("foo", Bindable.of( + MultipleConstructorsBean::class.java)) + assertThat(bindable.isBound).isFalse() + } + + @Test + fun `Bind to class with only default constructor should not bind`() { + val source = MockConfigurationPropertySource() + source.put("foo.int-value", "12") + val binder = Binder(source) + val bindable = binder.bind("foo", Bindable.of( + DefaultConstructorBean::class.java)) + assertThat(bindable.isBound).isFalse() + } + + @Test + fun `Bind to class should bind nested`() { + val source = MockConfigurationPropertySource() + source.put("foo.value-bean.int-value", "123") + source.put("foo.value-bean.long-value", "34") + source.put("foo.value-bean.boolean-value", "true") + source.put("foo.value-bean.string-value", "foo") + val binder = Binder(source) + val bean = binder.bind("foo", Bindable.of(ExampleNestedBean::class.java)).get() + assertThat(bean.valueBean.intValue).isEqualTo(123) + assertThat(bean.valueBean.longValue).isEqualTo(34) + assertThat(bean.valueBean.booleanValue).isTrue() + assertThat(bean.valueBean.stringValue).isEqualTo("foo") + assertThat(bean.valueBean.enumValue).isNull() + } + + @Test + fun `Bind to class with no value for optional should use null`() { + val source = MockConfigurationPropertySource() + source.put("foo.string-value", "foo") + val binder = Binder(source) + val bean = binder.bind("foo", Bindable.of( + ExampleValueBean::class.java)).get() + assertThat(bean.intValue).isNull() + assertThat(bean.longValue).isNull() + assertThat(bean.booleanValue).isNull() + assertThat(bean.stringValue).isEqualTo("foo") + assertThat(bean.enumValue).isNull() + } + + @Test + fun `Bind to class with no value for primitive should use default value`() { + val source = MockConfigurationPropertySource() + source.put("foo.string-value", "foo") + val binder = Binder(source) + val bean = binder.bind("foo", Bindable.of( + ExamplePrimitiveDefaultBean::class.java)).get() + assertThat(bean.intValue).isEqualTo(0) + assertThat(bean.longValue).isEqualTo(0) + assertThat(bean.booleanValue).isFalse() + assertThat(bean.stringValue).isEqualTo("foo") + assertThat(bean.enumValue).isNull() + } + + @Test + fun `Bind to class with no value and default value should use default value`() { + val source = MockConfigurationPropertySource() + source.put("foo.string-value", "foo") + val binder = Binder(source) + val bean = binder.bind("foo", Bindable.of( + ExampleDefaultValueBean::class.java)).get() + assertThat(bean.intValue).isEqualTo(5) + assertThat(bean.stringsList).containsOnly("a", "b", "c") + assertThat(bean.customList).containsOnly("x,y,z") + } + + @Test + fun `Bind to data class with no value should use default value`() { + val source = MockConfigurationPropertySource() + source.put("foo.enum-value", "foo-bar") + val binder = Binder(source) + val bean = binder.bind("foo", Bindable.of(ExampleDataClassBean::class.java)).get() + assertThat(bean.intValue).isEqualTo(5) + assertThat(bean.longValue).isEqualTo(42) + assertThat(bean.booleanValue).isFalse() + assertThat(bean.stringValue).isEqualTo("my data") + assertThat(bean.enumValue).isEqualTo(ExampleEnum.FOO_BAR) + } + + class ExampleValueBean(val intValue: Int?, val longValue: Long?, + val booleanValue: Boolean?, val stringValue: String?, + val enumValue: ExampleEnum?) + + class ExamplePrimitiveDefaultBean(val intValue: Int = 0, val longValue: Long = 0, + val booleanValue: Boolean = false, val stringValue: String?, + val enumValue: ExampleEnum?) + + + enum class ExampleEnum { + + FOO_BAR, + + BAR_BAZ + } + + @Suppress("unused", "UNUSED_PARAMETER") + class MultipleConstructorsWithPrimaryConstructorBean(val intValue: Int) { + constructor(intValue: Int, longValue: Long) : this(intValue) + } + + @Suppress("unused", "UNUSED_PARAMETER") + class MultipleConstructorsBean { + constructor(intValue: Int) + + constructor(intValue: Int, longValue: Long) : this(intValue) + } + + class DefaultConstructorBean {} + + class ExampleNestedBean(val valueBean: ExampleValueBean) + + class ExampleDefaultValueBean(val intValue: Int = 5, + val stringsList: List = listOf("a", "b", "c"), + val customList: List = listOf("x,y,z")) + + data class ExampleDataClassBean(val intValue: Int = 5, val longValue: Long = 42, + val booleanValue: Boolean = false, + val stringValue: String = "my data", + val enumValue: ExampleEnum = ExampleEnum.BAR_BAZ) + +} \ No newline at end of file