Add constructor based binding support
See gh-8762 Co-authored-by: Stephane Nicoll <snicoll@pivotal.io>
This commit is contained in:
committed by
Stephane Nicoll
parent
8948c1dac6
commit
7ca589d43c
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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<BeanBinder> binders = new ArrayList<>();
|
||||
binders.add(new ConstructorParametersBinder());
|
||||
binders.add(new JavaBeanBinder());
|
||||
BEAN_BINDERS = Collections.unmodifiableList(binders);
|
||||
}
|
||||
|
||||
@@ -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<Class<?>, Object> DEFAULT_TYPE_VALUES;
|
||||
|
||||
static {
|
||||
Map<Class<?>, 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> T bind(ConfigurationPropertyName name, Bindable<T> target,
|
||||
Binder.Context context, BeanPropertyBinder propertyBinder) {
|
||||
Bean bean = Bean.get(target);
|
||||
if (bean == null) {
|
||||
return null;
|
||||
}
|
||||
List<Object> bound = bind(propertyBinder, bean, context.getConverter());
|
||||
return (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray());
|
||||
}
|
||||
|
||||
private List<Object> bind(BeanPropertyBinder propertyBinder, Bean bean,
|
||||
BindConverter converter) {
|
||||
List<Object> 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<String, ConstructorParameter> parameters;
|
||||
|
||||
private Bean(boolean kotlinType, Constructor<?> constructor,
|
||||
Map<String, ConstructorParameter> 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<String, ConstructorParameter> parseParameters(
|
||||
Constructor<?> constructor) {
|
||||
Map<String, ConstructorParameter> 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<String, ConstructorParameter> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ConfigurationPropertySource> 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<String> stringsList;
|
||||
|
||||
private final List<String> customList;
|
||||
|
||||
public ExampleDefaultValueBean(
|
||||
@ConfigurationPropertyDefaultValue("5") int intValue,
|
||||
@ConfigurationPropertyDefaultValue({ "a", "b",
|
||||
"c" }) List<String> stringsList,
|
||||
@ConfigurationPropertyDefaultValue("x,y,z") List<String> customList) {
|
||||
this.intValue = intValue;
|
||||
this.stringsList = stringsList;
|
||||
this.customList = customList;
|
||||
}
|
||||
|
||||
public int getIntValue() {
|
||||
return this.intValue;
|
||||
}
|
||||
|
||||
public List<String> getStringsList() {
|
||||
return this.stringsList;
|
||||
}
|
||||
|
||||
public List<String> getCustomList() {
|
||||
return this.customList;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ExampleWithNonDefaultConstructor> 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
|
||||
|
||||
@@ -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<String> = listOf("a", "b", "c"),
|
||||
val customList: List<String> = 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)
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user