Commit c6dae576 authored by Madhura Bhave's avatar Madhura Bhave Committed by Phillip Webb

Add bindOrCreate for constructor based binding

Deprecate the existing `BindResult.orElseCreate` method in favor of
`bindOrCreate` methods on the `Binder`. These new methods allow us to
implement custom creation logic depending on the type of object being
bound. Specifically, it allows constructor based binding to create new
instances that respect the `@DefaultValue` annotations.

Closes gh-17098
Co-authored-by: 's avatarPhillip Webb <pwebb@pivotal.io>
parent 38fb6391
...@@ -53,8 +53,7 @@ public abstract class PathBasedTemplateAvailabilityProvider implements TemplateA ...@@ -53,8 +53,7 @@ public abstract class PathBasedTemplateAvailabilityProvider implements TemplateA
ResourceLoader resourceLoader) { ResourceLoader resourceLoader) {
if (ClassUtils.isPresent(this.className, classLoader)) { if (ClassUtils.isPresent(this.className, classLoader)) {
Binder binder = Binder.get(environment); Binder binder = Binder.get(environment);
TemplateAvailabilityProperties properties = binder.bind(this.propertyPrefix, this.propertiesClass) TemplateAvailabilityProperties properties = binder.bindOrCreate(this.propertyPrefix, this.propertiesClass);
.orElseCreate(this.propertiesClass);
return isTemplateAvailable(view, resourceLoader, properties); return isTemplateAvailable(view, resourceLoader, properties);
} }
return false; return false;
......
...@@ -55,7 +55,7 @@ final class ConfigurationPropertiesBeanDefinition extends GenericBeanDefinition ...@@ -55,7 +55,7 @@ final class ConfigurationPropertiesBeanDefinition extends GenericBeanDefinition
ConfigurationPropertiesBinder binder = beanFactory.getBean(ConfigurationPropertiesBinder.BEAN_NAME, ConfigurationPropertiesBinder binder = beanFactory.getBean(ConfigurationPropertiesBinder.BEAN_NAME,
ConfigurationPropertiesBinder.class); ConfigurationPropertiesBinder.class);
try { try {
return binder.bind(bindable).orElseCreate(type); return binder.bindOrCreate(bindable);
} }
catch (Exception ex) { catch (Exception ex) {
throw new ConfigurationPropertiesBindException(beanName, type, annotation, ex); throw new ConfigurationPropertiesBindException(beanName, type, annotation, ex);
......
...@@ -86,11 +86,21 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { ...@@ -86,11 +86,21 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware {
} }
public <T> BindResult<T> bind(Bindable<T> target) { public <T> BindResult<T> bind(Bindable<T> target) {
ConfigurationProperties annotation = getAnnotation(target);
BindHandler bindHandler = getBindHandler(target, annotation);
return getBinder().bind(annotation.prefix(), target, bindHandler);
}
public <T> T bindOrCreate(Bindable<T> target) {
ConfigurationProperties annotation = getAnnotation(target);
BindHandler bindHandler = getBindHandler(target, annotation);
return getBinder().bindOrCreate(annotation.prefix(), target, bindHandler);
}
private <T> ConfigurationProperties getAnnotation(Bindable<?> target) {
ConfigurationProperties annotation = target.getAnnotation(ConfigurationProperties.class); ConfigurationProperties annotation = target.getAnnotation(ConfigurationProperties.class);
Assert.state(annotation != null, () -> "Missing @ConfigurationProperties on " + target); Assert.state(annotation != null, () -> "Missing @ConfigurationProperties on " + target);
List<Validator> validators = getValidators(target); return annotation;
BindHandler bindHandler = getBindHandler(annotation, validators);
return getBinder().bind(annotation.prefix(), target, bindHandler);
} }
private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext, private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext,
...@@ -101,6 +111,25 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { ...@@ -101,6 +111,25 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware {
return null; return null;
} }
private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
List<Validator> validators = getValidators(target);
BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
if (annotation.ignoreInvalidFields()) {
handler = new IgnoreErrorsBindHandler(handler);
}
if (!annotation.ignoreUnknownFields()) {
UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
handler = new NoUnboundElementsBindHandler(handler, filter);
}
if (!validators.isEmpty()) {
handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
}
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
handler = advisor.apply(handler);
}
return handler;
}
private List<Validator> getValidators(Bindable<?> target) { private List<Validator> getValidators(Bindable<?> target) {
List<Validator> validators = new ArrayList<>(3); List<Validator> validators = new ArrayList<>(3);
if (this.configurationPropertiesValidator != null) { if (this.configurationPropertiesValidator != null) {
...@@ -122,24 +151,6 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware { ...@@ -122,24 +151,6 @@ class ConfigurationPropertiesBinder implements ApplicationContextAware {
return this.jsr303Validator; return this.jsr303Validator;
} }
private BindHandler getBindHandler(ConfigurationProperties annotation, List<Validator> validators) {
BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
if (annotation.ignoreInvalidFields()) {
handler = new IgnoreErrorsBindHandler(handler);
}
if (!annotation.ignoreUnknownFields()) {
UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
handler = new NoUnboundElementsBindHandler(handler, filter);
}
if (!validators.isEmpty()) {
handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
}
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
handler = advisor.apply(handler);
}
return handler;
}
private List<ConfigurationPropertiesBindHandlerAdvisor> getBindHandlerAdvisors() { private List<ConfigurationPropertiesBindHandlerAdvisor> getBindHandlerAdvisors() {
return this.applicationContext.getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class).orderedStream() return this.applicationContext.getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class).orderedStream()
.collect(Collectors.toList()); .collect(Collectors.toList());
......
...@@ -39,4 +39,13 @@ interface BeanBinder { ...@@ -39,4 +39,13 @@ interface BeanBinder {
*/ */
<T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context, BeanPropertyBinder propertyBinder); <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context, BeanPropertyBinder propertyBinder);
/**
* Return a new instance for the specified type.
* @param type the type used for creating a new instance
* @param context the bind context
* @param <T> the source type
* @return the created instance
*/
<T> T create(Class<T> type, Context context);
} }
...@@ -60,10 +60,25 @@ public interface BindHandler { ...@@ -60,10 +60,25 @@ public interface BindHandler {
return result; return result;
} }
/**
* Called when binding of an element ends with an unbound result and a newly created
* instance is about to be returned. Implementations may change the ultimately
* returned result or perform addition validation.
* @param name the name of the element being bound
* @param target the item being bound
* @param context the bind context
* @param result the newly created instance (never {@code null})
* @return the actual result that should be used (must not be {@code null})
* @since 2.2.2
*/
default Object onCreate(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
return result;
}
/** /**
* Called when binding fails for any reason (including failures from * Called when binding fails for any reason (including failures from
* {@link #onSuccess} calls). Implementations may choose to swallow exceptions and * {@link #onSuccess} or {@link #onCreate} calls). Implementations may choose to
* return an alternative result. * swallow exceptions and return an alternative result.
* @param name the name of the element being bound * @param name the name of the element being bound
* @param target the item being bound * @param target the item being bound
* @param context the bind context * @param context the bind context
......
...@@ -118,7 +118,9 @@ public final class BindResult<T> { ...@@ -118,7 +118,9 @@ public final class BindResult<T> {
* value has been bound. * value has been bound.
* @param type the type to create if no value was bound * @param type the type to create if no value was bound
* @return the value, if bound, otherwise a new instance of {@code type} * @return the value, if bound, otherwise a new instance of {@code type}
* @deprecated since 2.2.0 in favor of {@link Binder#bindOrCreate}
*/ */
@Deprecated
public T orElseCreate(Class<? extends T> type) { public T orElseCreate(Class<? extends T> type) {
Assert.notNull(type, "Type must not be null"); Assert.notNull(type, "Type must not be null");
return (this.value != null) ? this.value : BeanUtils.instantiateClass(type); return (this.value != null) ? this.value : BeanUtils.instantiateClass(type);
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
package org.springframework.boot.context.properties.bind; package org.springframework.boot.context.properties.bind;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
...@@ -25,11 +24,9 @@ import java.util.Deque; ...@@ -25,11 +24,9 @@ import java.util.Deque;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream;
import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
...@@ -58,14 +55,7 @@ public class Binder { ...@@ -58,14 +55,7 @@ public class Binder {
private static final Set<Class<?>> NON_BEAN_CLASSES = Collections private static final Set<Class<?>> NON_BEAN_CLASSES = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class))); .unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class)));
private static final List<BeanBinder> BEAN_BINDERS; private static final BeanBinder[] BEAN_BINDERS = { new ConstructorParametersBinder(), new JavaBeanBinder() };
static {
List<BeanBinder> binders = new ArrayList<>();
binders.add(new ConstructorParametersBinder());
binders.add(new JavaBeanBinder());
BEAN_BINDERS = Collections.unmodifiableList(binders);
}
private final Iterable<ConfigurationPropertySource> sources; private final Iterable<ConfigurationPropertySource> sources;
...@@ -196,24 +186,89 @@ public class Binder { ...@@ -196,24 +186,89 @@ public class Binder {
* @return the binding result (never {@code null}) * @return the binding result (never {@code null})
*/ */
public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler) { public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler) {
T bound = bind(name, target, handler, false);
return BindResult.of(bound);
}
/**
* Bind the specified target {@link Class} using this binder's
* {@link ConfigurationPropertySource property sources} or create a new instance using
* the type of the {@link Bindable} if the result of the binding is {@code null}.
* @param name the configuration property name to bind
* @param target the target class
* @param <T> the bound type
* @return the bound or created object
* @since 2.2.0
* @see #bind(ConfigurationPropertyName, Bindable, BindHandler)
*/
public <T> T bindOrCreate(String name, Class<T> target) {
return bindOrCreate(name, Bindable.of(target));
}
/**
* Bind the specified target {@link Bindable} using this binder's
* {@link ConfigurationPropertySource property sources} or create a new instance using
* the type of the {@link Bindable} if the result of the binding is {@code null}.
* @param name the configuration property name to bind
* @param target the target bindable
* @param <T> the bound type
* @return the bound or created object
* @since 2.2.0
* @see #bindOrCreate(ConfigurationPropertyName, Bindable, BindHandler)
*/
public <T> T bindOrCreate(String name, Bindable<T> target) {
return bindOrCreate(ConfigurationPropertyName.of(name), target, null);
}
/**
* Bind the specified target {@link Bindable} using this binder's
* {@link ConfigurationPropertySource property sources} or create a new instance using
* the type of the {@link Bindable} if the result of the binding is {@code null}.
* @param name the configuration property name to bind
* @param target the target bindable
* @param handler the bind handler
* @param <T> the bound type
* @return the bound or created object
* @since 2.2.0
* @see #bindOrCreate(ConfigurationPropertyName, Bindable, BindHandler)
*/
public <T> T bindOrCreate(String name, Bindable<T> target, BindHandler handler) {
return bindOrCreate(ConfigurationPropertyName.of(name), target, handler);
}
/**
* Bind the specified target {@link Bindable} using this binder's
* {@link ConfigurationPropertySource property sources} or create a new instance using
* the type of the {@link Bindable} if the result of the binding is {@code null}.
* @param name the configuration property name to bind
* @param target the target bindable
* @param handler the bind handler (may be {@code null})
* @param <T> the bound or created type
* @since 2.2.0
* @return the bound or created object
*/
public <T> T bindOrCreate(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler) {
return bind(name, target, handler, true);
}
private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, boolean create) {
Assert.notNull(name, "Name must not be null"); Assert.notNull(name, "Name must not be null");
Assert.notNull(target, "Target must not be null"); Assert.notNull(target, "Target must not be null");
handler = (handler != null) ? handler : BindHandler.DEFAULT; handler = (handler != null) ? handler : BindHandler.DEFAULT;
Context context = new Context(); Context context = new Context();
T bound = bind(name, target, handler, context, false); return bind(name, target, handler, context, false, create);
return BindResult.of(bound);
} }
protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,
boolean allowRecursiveBinding) { boolean allowRecursiveBinding, boolean create) {
context.clearConfigurationProperty(); context.clearConfigurationProperty();
try { try {
target = handler.onStart(name, target, context); target = handler.onStart(name, target, context);
if (target == null) { if (target == null) {
return null; return handleBindResult(name, target, handler, context, null, create);
} }
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding); Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
return handleBindResult(name, target, handler, context, bound); return handleBindResult(name, target, handler, context, bound, create);
} }
catch (Exception ex) { catch (Exception ex) {
return handleBindError(name, target, handler, context, ex); return handleBindError(name, target, handler, context, ex);
...@@ -221,15 +276,32 @@ public class Binder { ...@@ -221,15 +276,32 @@ public class Binder {
} }
private <T> T handleBindResult(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, private <T> T handleBindResult(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
Context context, Object result) throws Exception { Context context, Object result, boolean create) throws Exception {
if (result != null) { if (result != null) {
result = handler.onSuccess(name, target, context, result); result = handler.onSuccess(name, target, context, result);
result = context.getConverter().convert(result, target); result = context.getConverter().convert(result, target);
} }
if (result == null && create) {
result = createBean(target, context);
result = handler.onCreate(name, target, context, result);
result = context.getConverter().convert(result, target);
Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
}
handler.onFinish(name, target, context, result); handler.onFinish(name, target, context, result);
return context.getConverter().convert(result, target); return context.getConverter().convert(result, target);
} }
private Object createBean(Bindable<?> target, Context context) {
Class<?> type = target.getType().resolve();
for (BeanBinder beanBinder : BEAN_BINDERS) {
Object bean = beanBinder.create(type, context);
if (bean != null) {
return bean;
}
}
return null;
}
private <T> T handleBindError(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, private <T> T handleBindError(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
Context context, Exception error) { Context context, Exception error) {
try { try {
...@@ -288,7 +360,7 @@ public class Binder { ...@@ -288,7 +360,7 @@ public class Binder {
Context context, AggregateBinder<?> aggregateBinder) { Context context, AggregateBinder<?> aggregateBinder) {
AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> { AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> {
boolean allowRecursiveBinding = aggregateBinder.isAllowRecursiveBinding(source); boolean allowRecursiveBinding = aggregateBinder.isAllowRecursiveBinding(source);
Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context, allowRecursiveBinding); Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context, allowRecursiveBinding, false);
return context.withSource(source, supplier); return context.withSource(source, supplier);
}; };
return context.withIncreasedDepth(() -> aggregateBinder.bind(name, target, elementBinder)); return context.withIncreasedDepth(() -> aggregateBinder.bind(name, target, elementBinder));
...@@ -325,10 +397,15 @@ public class Binder { ...@@ -325,10 +397,15 @@ public class Binder {
return null; return null;
} }
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
propertyTarget, handler, context, false); propertyTarget, handler, context, false, false);
return context.withBean(type, () -> { return context.withBean(type, () -> {
Stream<?> boundBeans = BEAN_BINDERS.stream().map((b) -> b.bind(name, target, context, propertyBinder)); for (BeanBinder beanBinder : BEAN_BINDERS) {
return boundBeans.filter(Objects::nonNull).findFirst().orElse(null); Object bean = beanBinder.bind(name, target, context, propertyBinder);
if (bean != null) {
return bean;
}
}
return null;
}); });
} }
......
...@@ -57,20 +57,45 @@ class ConstructorParametersBinder implements BeanBinder { ...@@ -57,20 +57,45 @@ class ConstructorParametersBinder implements BeanBinder {
return null; return null;
} }
List<Object> bound = bind(propertyBinder, bean, context.getConverter()); List<Object> bound = bind(propertyBinder, bean, context.getConverter());
return (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray()); return (bound != null) ? (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray()) : null;
}
@Override
@SuppressWarnings("unchecked")
public <T> T create(Class<T> type, Binder.Context context) {
Bean bean = getBean(type);
if (bean == null) {
return null;
}
Collection<ConstructorParameter> parameters = bean.getParameters().values();
List<Object> parameterValues = new ArrayList<>(parameters.size());
for (ConstructorParameter parameter : parameters) {
Object boundParameter = getDefaultValue(parameter, context.getConverter());
parameterValues.add(boundParameter);
}
return (T) BeanUtils.instantiateClass(bean.getConstructor(), parameterValues.toArray());
}
private <T> Bean getBean(Class<T> type) {
if (KOTLIN_PRESENT && KotlinDetector.isKotlinType(type)) {
return KotlinBeanProvider.get(type);
}
return SimpleBeanProvider.get(type);
} }
private List<Object> bind(BeanPropertyBinder propertyBinder, Bean bean, BindConverter converter) { private List<Object> bind(BeanPropertyBinder propertyBinder, Bean bean, BindConverter converter) {
Collection<ConstructorParameter> parameters = bean.getParameters().values(); Collection<ConstructorParameter> parameters = bean.getParameters().values();
List<Object> boundParameters = new ArrayList<>(parameters.size()); List<Object> boundParameters = new ArrayList<>(parameters.size());
int unboundParameterCount = 0;
for (ConstructorParameter parameter : parameters) { for (ConstructorParameter parameter : parameters) {
Object boundParameter = bind(parameter, propertyBinder); Object boundParameter = bind(parameter, propertyBinder);
if (boundParameter == null) { if (boundParameter == null) {
unboundParameterCount++;
boundParameter = getDefaultValue(parameter, converter); boundParameter = getDefaultValue(parameter, converter);
} }
boundParameters.add(boundParameter); boundParameters.add(boundParameter);
} }
return boundParameters; return (unboundParameterCount != parameters.size()) ? boundParameters : null;
} }
private Object getDefaultValue(ConstructorParameter parameter, BindConverter converter) { private Object getDefaultValue(ConstructorParameter parameter, BindConverter converter) {
......
...@@ -55,6 +55,11 @@ class JavaBeanBinder implements BeanBinder { ...@@ -55,6 +55,11 @@ class JavaBeanBinder implements BeanBinder {
return (bound ? beanSupplier.get() : null); return (bound ? beanSupplier.get() : null);
} }
@Override
public <T> T create(Class<T> type, Context context) {
return BeanUtils.instantiateClass(type);
}
private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) { private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
for (ConfigurationPropertySource source : context.getSources()) { for (ConfigurationPropertySource source : context.getSources()) {
if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) { if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) {
......
...@@ -151,6 +151,8 @@ class BindResultTests { ...@@ -151,6 +151,8 @@ class BindResultTests {
} }
@Test @Test
@Deprecated
@SuppressWarnings("deprecation")
void orElseCreateWhenTypeIsNullShouldThrowException() { void orElseCreateWhenTypeIsNullShouldThrowException() {
BindResult<String> result = BindResult.of("foo"); BindResult<String> result = BindResult.of("foo");
assertThatIllegalArgumentException().isThrownBy(() -> result.orElseCreate(null)) assertThatIllegalArgumentException().isThrownBy(() -> result.orElseCreate(null))
...@@ -158,12 +160,14 @@ class BindResultTests { ...@@ -158,12 +160,14 @@ class BindResultTests {
} }
@Test @Test
@Deprecated
void orElseCreateWhenHasValueShouldReturnValue() { void orElseCreateWhenHasValueShouldReturnValue() {
BindResult<ExampleBean> result = BindResult.of(new ExampleBean("foo")); BindResult<ExampleBean> result = BindResult.of(new ExampleBean("foo"));
assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("foo"); assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("foo");
} }
@Test @Test
@Deprecated
void orElseCreateWhenHasValueNoShouldReturnCreatedValue() { void orElseCreateWhenHasValueNoShouldReturnCreatedValue() {
BindResult<ExampleBean> result = BindResult.of(null); BindResult<ExampleBean> result = BindResult.of(null);
assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("new"); assertThat(result.orElseCreate(ExampleBean.class).getValue()).isEqualTo("new");
......
...@@ -162,6 +162,15 @@ class BinderTests { ...@@ -162,6 +162,15 @@ class BinderTests {
ordered.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), eq(1)); ordered.verify(handler).onSuccess(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), eq(1));
} }
@Test
void bindOrCreateWhenNotBoundShouldTriggerOnCreate() {
BindHandler handler = mock(BindHandler.class, Answers.CALLS_REAL_METHODS);
Bindable<JavaBean> target = Bindable.of(JavaBean.class);
this.binder.bindOrCreate("foo", target, handler);
InOrder ordered = inOrder(handler);
ordered.verify(handler).onCreate(eq(ConfigurationPropertyName.of("foo")), eq(target), any(), any());
}
@Test @Test
void bindToJavaBeanShouldReturnPopulatedBean() { void bindToJavaBeanShouldReturnPopulatedBean() {
this.sources.add(new MockConfigurationPropertySource("foo.value", "bar")); this.sources.add(new MockConfigurationPropertySource("foo.value", "bar"));
...@@ -280,6 +289,21 @@ class BinderTests { ...@@ -280,6 +289,21 @@ class BinderTests {
assertThat(result.getValue()).isEqualTo("hello"); assertThat(result.getValue()).isEqualTo("hello");
} }
@Test
void bindOrCreateWhenBindSuccessfulShouldReturnBoundValue() {
this.sources.add(new MockConfigurationPropertySource("foo.value", "bar"));
JavaBean result = this.binder.bindOrCreate("foo", Bindable.of(JavaBean.class));
assertThat(result.getValue()).isEqualTo("bar");
assertThat(result.getItems()).isEmpty();
}
@Test
void bindOrCreateWhenUnboundShouldReturnCreatedValue() {
JavaBean value = this.binder.bindOrCreate("foo", Bindable.of(JavaBean.class));
assertThat(value).isNotNull();
assertThat(value).isInstanceOf(JavaBean.class);
}
public static class JavaBean { public static class JavaBean {
private String value; private String value;
...@@ -300,6 +324,40 @@ class BinderTests { ...@@ -300,6 +324,40 @@ class BinderTests {
} }
public static class NestedJavaBean {
private DefaultValuesBean valuesBean = new DefaultValuesBean();
public DefaultValuesBean getValuesBean() {
return this.valuesBean;
}
public void setValuesBean(DefaultValuesBean valuesBean) {
this.valuesBean = valuesBean;
}
}
public static class DefaultValuesBean {
private String value = "hello";
private List<String> items = Collections.emptyList();
public String getValue() {
return this.value;
}
public void setValue(String value) {
this.value = value;
}
public List<String> getItems() {
return this.items;
}
}
public enum ExampleEnum { public enum ExampleEnum {
FOO_BAR, BAR_BAZ, BAZ_BOO FOO_BAR, BAR_BAZ, BAZ_BOO
......
...@@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.bind; ...@@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.bind;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
...@@ -129,14 +130,20 @@ class ConstructorParametersBinderTests { ...@@ -129,14 +130,20 @@ class ConstructorParametersBinderTests {
} }
@Test @Test
void bindToClassWithNoValueAndDefaultValueShouldUseDefault() { void bindToClassWithNoValueAndDefaultValueShouldNotBind() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource(); MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.string-value", "foo"); source.put("foo.string-value", "foo");
this.sources.add(source); this.sources.add(source);
ExampleDefaultValueBean bean = this.binder.bind("foo", Bindable.of(ExampleDefaultValueBean.class)).get(); assertThat(this.binder.bind("foo", Bindable.of(ExampleDefaultValueBean.class)).isBound()).isFalse();
assertThat(bean.getIntValue()).isEqualTo(5); }
assertThat(bean.getStringsList()).containsOnly("a", "b", "c");
assertThat(bean.getCustomList()).containsOnly("x,y,z"); @Test
void bindToClassWhenNoParameterBoundShouldReturnNull() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
this.sources.add(source.nonIterable());
BindResult<ExampleFailingConstructorBean> result = this.binder.bind("foo",
Bindable.of(ExampleFailingConstructorBean.class));
assertThat(result.isBound()).isFalse();
} }
@Test @Test
...@@ -149,6 +156,47 @@ class ConstructorParametersBinderTests { ...@@ -149,6 +156,47 @@ class ConstructorParametersBinderTests {
assertThat(bean.getDate().toString()).isEqualTo("2014-04-01"); assertThat(bean.getDate().toString()).isEqualTo("2014-04-01");
} }
@Test
void bindWithAnnotationsAndDefaultValue() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.bar", "hello");
this.sources.add(source);
ConverterAnnotatedExampleBean bean = this.binder.bind("foo", Bindable.of(ConverterAnnotatedExampleBean.class))
.get();
assertThat(bean.getDate().toString()).isEqualTo("2019-05-10");
}
@Test
void createShouldReturnCreatedValue() {
ExampleValueBean value = this.binder.bindOrCreate("foo", Bindable.of(ExampleValueBean.class));
assertThat(value.getIntValue()).isEqualTo(0);
assertThat(value.getLongValue()).isEqualTo(0);
assertThat(value.isBooleanValue()).isEqualTo(false);
assertThat(value.getStringValue()).isNull();
assertThat(value.getEnumValue()).isNull();
}
@Test
void createWithNestedShouldReturnCreatedValue() {
ExampleNestedBean value = this.binder.bindOrCreate("foo", Bindable.of(ExampleNestedBean.class));
assertThat(value.getValueBean()).isEqualTo(null);
}
@Test
void createWithDefaultValuesShouldReturnCreatedWithDefaultValues() {
ExampleDefaultValueBean value = this.binder.bindOrCreate("foo", Bindable.of(ExampleDefaultValueBean.class));
assertThat(value.getIntValue()).isEqualTo(5);
assertThat(value.getStringsList()).containsOnly("a", "b", "c");
assertThat(value.getCustomList()).containsOnly("x,y,z");
}
@Test
void createWithDefaultValuesAndAnnotationsShouldReturnCreatedWithDefaultValues() {
ConverterAnnotatedExampleBean bean = this.binder.bindOrCreate("foo",
Bindable.of(ConverterAnnotatedExampleBean.class));
assertThat(bean.getDate().toString()).isEqualTo("2019-05-10");
}
public static class ExampleValueBean { public static class ExampleValueBean {
private final int intValue; private final int intValue;
...@@ -277,18 +325,49 @@ class ConstructorParametersBinderTests { ...@@ -277,18 +325,49 @@ class ConstructorParametersBinderTests {
} }
public static class ExampleFailingConstructorBean {
private final String name;
private final Object value;
ExampleFailingConstructorBean(String name, String value) {
Objects.requireNonNull(name, "'name' must be not null.");
Objects.requireNonNull(value, "'value' must be not null.");
this.name = name;
this.value = value;
}
public String getName() {
return this.name;
}
public Object getValue() {
return this.value;
}
}
public static class ConverterAnnotatedExampleBean { public static class ConverterAnnotatedExampleBean {
private final LocalDate date; private final LocalDate date;
ConverterAnnotatedExampleBean(@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { private final String bar;
ConverterAnnotatedExampleBean(
@DefaultValue("2019-05-10") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, String bar) {
this.date = date; this.date = date;
this.bar = bar;
} }
public LocalDate getDate() { public LocalDate getDate() {
return this.date; return this.date;
} }
public String getBar() {
return this.bar;
}
} }
} }
...@@ -523,6 +523,12 @@ class JavaBeanBinderTests { ...@@ -523,6 +523,12 @@ class JavaBeanBinderTests {
property.setValue(() -> target, "some string"); property.setValue(() -> target, "some string");
} }
@Test
void bindOrCreateWithNestedShouldReturnCreatedValue() {
NestedJavaBean result = this.binder.bindOrCreate("foo", Bindable.of(NestedJavaBean.class));
assertThat(result.getNested().getBar()).isEqualTo(456);
}
public static class ExampleValueBean { public static class ExampleValueBean {
private int intValue; private int intValue;
...@@ -991,4 +997,18 @@ class JavaBeanBinderTests { ...@@ -991,4 +997,18 @@ class JavaBeanBinderTests {
} }
public static class NestedJavaBean {
private ExampleDefaultsBean nested = new ExampleDefaultsBean();
public ExampleDefaultsBean getNested() {
return this.nested;
}
public void setNested(ExampleDefaultsBean nested) {
this.nested = nested;
}
}
} }
...@@ -140,12 +140,21 @@ class KotlinConstructorParametersBinderTests { ...@@ -140,12 +140,21 @@ class KotlinConstructorParametersBinderTests {
} }
@Test @Test
fun `Bind to class with no value and default value should use default value`() { fun `Bind to class with no value and default value should return unbound`() {
val source = MockConfigurationPropertySource() val source = MockConfigurationPropertySource()
source.put("foo.string-value", "foo") source.put("foo.string-value", "foo")
val binder = Binder(source) val binder = Binder(source)
val bean = binder.bind("foo", Bindable.of( assertThat(binder.bind("foo", Bindable.of(
ExampleDefaultValueBean::class.java)).get() ExampleDefaultValueBean::class.java)).isBound()).isFalse();
}
@Test
fun `Bind or create to class with no value and default value should return default value`() {
val source = MockConfigurationPropertySource()
source.put("foo.string-value", "foo")
val binder = Binder(source)
val bean = binder.bindOrCreate("foo", Bindable.of(
ExampleDefaultValueBean::class.java))
assertThat(bean.intValue).isEqualTo(5) assertThat(bean.intValue).isEqualTo(5)
assertThat(bean.stringsList).containsOnly("a", "b", "c") assertThat(bean.stringsList).containsOnly("a", "b", "c")
assertThat(bean.customList).containsOnly("x,y,z") assertThat(bean.customList).containsOnly("x,y,z")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment