Polish Binder classes
Polish and rename some of the internal Binder classes to better reflect their purpose. The `BeanBinder` is now called `DataObjectBinder` and as a `JavaBeanBinder` implementation for setter based properties, and a `ValueObjectBinder` implementation for constructor based properties.
This commit is contained in:
parent
c6dae5764e
commit
0b3015e4ff
|
@ -55,7 +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 BeanBinder[] BEAN_BINDERS = { new ConstructorParametersBinder(), new JavaBeanBinder() };
|
private static final DataObjectBinder[] DATA_OBJECT_BINDERS = { new ValueObjectBinder(), new JavaBeanBinder() };
|
||||||
|
|
||||||
private final Iterable<ConfigurationPropertySource> sources;
|
private final Iterable<ConfigurationPropertySource> sources;
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ public class Binder {
|
||||||
result = context.getConverter().convert(result, target);
|
result = context.getConverter().convert(result, target);
|
||||||
}
|
}
|
||||||
if (result == null && create) {
|
if (result == null && create) {
|
||||||
result = createBean(target, context);
|
result = create(target, context);
|
||||||
result = handler.onCreate(name, target, context, result);
|
result = handler.onCreate(name, target, context, result);
|
||||||
result = context.getConverter().convert(result, target);
|
result = context.getConverter().convert(result, target);
|
||||||
Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
|
Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
|
||||||
|
@ -291,12 +291,11 @@ public class Binder {
|
||||||
return context.getConverter().convert(result, target);
|
return context.getConverter().convert(result, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object createBean(Bindable<?> target, Context context) {
|
private Object create(Bindable<?> target, Context context) {
|
||||||
Class<?> type = target.getType().resolve();
|
for (DataObjectBinder dataObjectBinder : DATA_OBJECT_BINDERS) {
|
||||||
for (BeanBinder beanBinder : BEAN_BINDERS) {
|
Object instance = dataObjectBinder.create(target, context);
|
||||||
Object bean = beanBinder.create(type, context);
|
if (instance != null) {
|
||||||
if (bean != null) {
|
return instance;
|
||||||
return bean;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -331,15 +330,15 @@ public class Binder {
|
||||||
return bindProperty(target, context, property);
|
return bindProperty(target, context, property);
|
||||||
}
|
}
|
||||||
catch (ConverterNotFoundException ex) {
|
catch (ConverterNotFoundException ex) {
|
||||||
// We might still be able to bind it as a bean
|
// We might still be able to bind it using the recursive binders
|
||||||
Object bean = bindBean(name, target, handler, context, allowRecursiveBinding);
|
Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding);
|
||||||
if (bean != null) {
|
if (instance != null) {
|
||||||
return bean;
|
return instance;
|
||||||
}
|
}
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bindBean(name, target, handler, context, allowRecursiveBinding);
|
return bindDataObject(name, target, handler, context, allowRecursiveBinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context context) {
|
private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context context) {
|
||||||
|
@ -387,22 +386,22 @@ public class Binder {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object bindBean(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler, Context context,
|
private Object bindDataObject(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler,
|
||||||
boolean allowRecursiveBinding) {
|
Context context, boolean allowRecursiveBinding) {
|
||||||
if (isUnbindableBean(name, target, context)) {
|
if (isUnbindableBean(name, target, context)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Class<?> type = target.getType().resolve(Object.class);
|
Class<?> type = target.getType().resolve(Object.class);
|
||||||
if (!allowRecursiveBinding && context.hasBoundBean(type)) {
|
if (!allowRecursiveBinding && context.isBindingDataObject(type)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
|
DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
|
||||||
propertyTarget, handler, context, false, false);
|
propertyTarget, handler, context, false, false);
|
||||||
return context.withBean(type, () -> {
|
return context.withDataObject(type, () -> {
|
||||||
for (BeanBinder beanBinder : BEAN_BINDERS) {
|
for (DataObjectBinder dataObjectBinder : DATA_OBJECT_BINDERS) {
|
||||||
Object bean = beanBinder.bind(name, target, context, propertyBinder);
|
Object instance = dataObjectBinder.bind(name, target, context, propertyBinder);
|
||||||
if (bean != null) {
|
if (instance != null) {
|
||||||
return bean;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -457,7 +456,7 @@ public class Binder {
|
||||||
|
|
||||||
private int sourcePushCount;
|
private int sourcePushCount;
|
||||||
|
|
||||||
private final Deque<Class<?>> beans = new ArrayDeque<>();
|
private final Deque<Class<?>> dataObjectBindings = new ArrayDeque<>();
|
||||||
|
|
||||||
private ConfigurationProperty configurationProperty;
|
private ConfigurationProperty configurationProperty;
|
||||||
|
|
||||||
|
@ -487,18 +486,18 @@ public class Binder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T withBean(Class<?> bean, Supplier<T> supplier) {
|
private <T> T withDataObject(Class<?> type, Supplier<T> supplier) {
|
||||||
this.beans.push(bean);
|
this.dataObjectBindings.push(type);
|
||||||
try {
|
try {
|
||||||
return withIncreasedDepth(supplier);
|
return withIncreasedDepth(supplier);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
this.beans.pop();
|
this.dataObjectBindings.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasBoundBean(Class<?> bean) {
|
private boolean isBindingDataObject(Class<?> type) {
|
||||||
return this.beans.contains(bean);
|
return this.dataObjectBindings.contains(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T withIncreasedDepth(Supplier<T> supplier) {
|
private <T> T withIncreasedDepth(Supplier<T> supplier) {
|
||||||
|
|
|
@ -1,262 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* https://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.lang.reflect.Type;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import kotlin.reflect.KFunction;
|
|
||||||
import kotlin.reflect.KParameter;
|
|
||||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
|
||||||
|
|
||||||
import org.springframework.beans.BeanUtils;
|
|
||||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
|
||||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
|
||||||
import org.springframework.core.KotlinDetector;
|
|
||||||
import org.springframework.core.ParameterNameDiscoverer;
|
|
||||||
import org.springframework.core.ResolvableType;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link BeanBinder} for constructor based binding.
|
|
||||||
*
|
|
||||||
* @author Madhura Bhave
|
|
||||||
* @author Stephane Nicoll
|
|
||||||
*/
|
|
||||||
class ConstructorParametersBinder implements BeanBinder {
|
|
||||||
|
|
||||||
private static boolean KOTLIN_PRESENT = KotlinDetector.isKotlinPresent();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
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 (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) {
|
|
||||||
Collection<ConstructorParameter> parameters = bean.getParameters().values();
|
|
||||||
List<Object> boundParameters = new ArrayList<>(parameters.size());
|
|
||||||
int unboundParameterCount = 0;
|
|
||||||
for (ConstructorParameter parameter : parameters) {
|
|
||||||
Object boundParameter = bind(parameter, propertyBinder);
|
|
||||||
if (boundParameter == null) {
|
|
||||||
unboundParameterCount++;
|
|
||||||
boundParameter = getDefaultValue(parameter, converter);
|
|
||||||
}
|
|
||||||
boundParameters.add(boundParameter);
|
|
||||||
}
|
|
||||||
return (unboundParameterCount != parameters.size()) ? boundParameters : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object getDefaultValue(ConstructorParameter parameter, BindConverter converter) {
|
|
||||||
if (parameter.getDefaultValue() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return converter.convert(parameter.getDefaultValue(), parameter.getType(), parameter.getAnnotations());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object bind(ConstructorParameter parameter, BeanPropertyBinder propertyBinder) {
|
|
||||||
String propertyName = parameter.getName();
|
|
||||||
ResolvableType type = parameter.getType();
|
|
||||||
return propertyBinder.bindProperty(propertyName, Bindable.of(type).withAnnotations(parameter.getAnnotations()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Bean {
|
|
||||||
|
|
||||||
private final Constructor<?> constructor;
|
|
||||||
|
|
||||||
private final Map<String, ConstructorParameter> parameters;
|
|
||||||
|
|
||||||
private Bean(Constructor<?> constructor, Map<String, ConstructorParameter> parameters) {
|
|
||||||
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)) {
|
|
||||||
return KotlinBeanProvider.get(type);
|
|
||||||
}
|
|
||||||
return SimpleBeanProvider.get(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, ConstructorParameter> getParameters() {
|
|
||||||
return this.parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Constructor<?> getConstructor() {
|
|
||||||
return this.constructor;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bean provider for a Kotlin class. Uses the Kotlin constructor to extract the
|
|
||||||
* parameter names.
|
|
||||||
*/
|
|
||||||
private static class KotlinBeanProvider {
|
|
||||||
|
|
||||||
public static Bean get(Class<?> type) {
|
|
||||||
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
|
|
||||||
if (primaryConstructor != null && primaryConstructor.getParameterCount() > 0) {
|
|
||||||
return get(primaryConstructor);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Bean get(Constructor<?> constructor) {
|
|
||||||
KFunction<?> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(constructor);
|
|
||||||
if (kotlinConstructor != null) {
|
|
||||||
return new Bean(constructor, parseParameters(kotlinConstructor));
|
|
||||||
}
|
|
||||||
return SimpleBeanProvider.get(constructor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, ConstructorParameter> parseParameters(KFunction<?> constructor) {
|
|
||||||
Map<String, ConstructorParameter> parameters = new LinkedHashMap<>();
|
|
||||||
for (KParameter parameter : constructor.getParameters()) {
|
|
||||||
String name = parameter.getName();
|
|
||||||
Type type = ReflectJvmMapping.getJavaType(parameter.getType());
|
|
||||||
Annotation[] annotations = parameter.getAnnotations().toArray(new Annotation[0]);
|
|
||||||
parameters.computeIfAbsent(name,
|
|
||||||
(s) -> new ConstructorParameter(name, ResolvableType.forType(type), annotations, null));
|
|
||||||
}
|
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple bean provider that uses {@link DefaultParameterNameDiscoverer} to extract
|
|
||||||
* the parameter names.
|
|
||||||
*/
|
|
||||||
private static class SimpleBeanProvider {
|
|
||||||
|
|
||||||
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
|
|
||||||
|
|
||||||
public static Bean get(Class<?> type) {
|
|
||||||
Constructor<?>[] constructors = type.getDeclaredConstructors();
|
|
||||||
if (constructors.length == 1 && constructors[0].getParameterCount() > 0) {
|
|
||||||
return SimpleBeanProvider.get(constructors[0]);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Bean get(Constructor<?> constructor) {
|
|
||||||
return new Bean(constructor, parseParameters(constructor));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, ConstructorParameter> parseParameters(Constructor<?> constructor) {
|
|
||||||
String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor);
|
|
||||||
Assert.state(parameterNames != null, () -> "Failed to extract parameter names for " + constructor);
|
|
||||||
Map<String, ConstructorParameter> parametersByName = new LinkedHashMap<>();
|
|
||||||
Parameter[] parameters = constructor.getParameters();
|
|
||||||
for (int i = 0; i < parameterNames.length; i++) {
|
|
||||||
String name = parameterNames[i];
|
|
||||||
Parameter parameter = parameters[i];
|
|
||||||
DefaultValue[] annotationsByType = parameter.getAnnotationsByType(DefaultValue.class);
|
|
||||||
String[] defaultValue = (annotationsByType.length > 0) ? annotationsByType[0].value() : null;
|
|
||||||
parametersByName.computeIfAbsent(name,
|
|
||||||
(key) -> new ConstructorParameter(name, ResolvableType.forClass(parameter.getType()),
|
|
||||||
parameter.getDeclaredAnnotations(), defaultValue));
|
|
||||||
}
|
|
||||||
return parametersByName;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -20,15 +20,18 @@ import org.springframework.boot.context.properties.bind.Binder.Context;
|
||||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal strategy used by {@link Binder} to bind beans.
|
* Internal strategy used by {@link Binder} to bind data objects. A data object is an
|
||||||
|
* object composed itself of recursively bound properties.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
|
* @see JavaBeanBinder
|
||||||
|
* @see ValueObjectBinder
|
||||||
*/
|
*/
|
||||||
interface BeanBinder {
|
interface DataObjectBinder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a bound bean instance or {@code null} if the {@link BeanBinder} does not
|
* Return a bound instance or {@code null} if the {@link DataObjectBinder} does not
|
||||||
* support the specified {@link Bindable}.
|
* support the specified {@link Bindable}.
|
||||||
* @param name the name being bound
|
* @param name the name being bound
|
||||||
* @param target the bindable to bind
|
* @param target the bindable to bind
|
||||||
|
@ -37,15 +40,17 @@ interface BeanBinder {
|
||||||
* @param <T> the source type
|
* @param <T> the source type
|
||||||
* @return a bound instance or {@code null}
|
* @return a bound instance or {@code null}
|
||||||
*/
|
*/
|
||||||
<T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context, BeanPropertyBinder propertyBinder);
|
<T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
|
||||||
|
DataObjectPropertyBinder propertyBinder);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a new instance for the specified type.
|
* Return a newly created instance or {@code null} if the {@link DataObjectBinder}
|
||||||
* @param type the type used for creating a new instance
|
* does not support the specified {@link Bindable}.
|
||||||
|
* @param target the bindable to create
|
||||||
* @param context the bind context
|
* @param context the bind context
|
||||||
* @param <T> the source type
|
* @param <T> the source type
|
||||||
* @return the created instance
|
* @return the created instance
|
||||||
*/
|
*/
|
||||||
<T> T create(Class<T> type, Context context);
|
<T> T create(Bindable<T> target, Context context);
|
||||||
|
|
||||||
}
|
}
|
|
@ -17,13 +17,13 @@
|
||||||
package org.springframework.boot.context.properties.bind;
|
package org.springframework.boot.context.properties.bind;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binder that can be used by {@link BeanBinder} implementations to recursively bind bean
|
* Binder that can be used by {@link DataObjectBinder} implementations to bind the data
|
||||||
* properties.
|
* object properties.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
interface BeanPropertyBinder {
|
interface DataObjectPropertyBinder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind the given property.
|
* Bind the given property.
|
|
@ -17,14 +17,15 @@
|
||||||
package org.springframework.boot.context.properties.bind;
|
package org.springframework.boot.context.properties.bind;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal utility to help when dealing with Java Bean property names.
|
* Internal utility to help when dealing with data object property names.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
|
* @see DataObjectBinder
|
||||||
*/
|
*/
|
||||||
abstract class BeanPropertyName {
|
abstract class DataObjectPropertyName {
|
||||||
|
|
||||||
private BeanPropertyName() {
|
private DataObjectPropertyName() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,19 +34,9 @@ abstract class BeanPropertyName {
|
||||||
* @return the dashed from
|
* @return the dashed from
|
||||||
*/
|
*/
|
||||||
public static String toDashedForm(String name) {
|
public static String toDashedForm(String name) {
|
||||||
return toDashedForm(name, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the specified Java Bean property name in dashed form.
|
|
||||||
* @param name the source name
|
|
||||||
* @param start the starting char
|
|
||||||
* @return the dashed from
|
|
||||||
*/
|
|
||||||
public static String toDashedForm(String name, int start) {
|
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
String replaced = name.replace('_', '-');
|
String replaced = name.replace('_', '-');
|
||||||
for (int i = start; i < replaced.length(); i++) {
|
for (int i = 0; i < replaced.length(); i++) {
|
||||||
char ch = replaced.charAt(i);
|
char ch = replaced.charAt(i);
|
||||||
if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') {
|
if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') {
|
||||||
result.append('-');
|
result.append('-');
|
|
@ -35,16 +35,16 @@ import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link BeanBinder} for mutable Java Beans.
|
* {@link DataObjectBinder} for mutable Java Beans.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
class JavaBeanBinder implements BeanBinder {
|
class JavaBeanBinder implements DataObjectBinder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
|
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
|
||||||
BeanPropertyBinder propertyBinder) {
|
DataObjectPropertyBinder propertyBinder) {
|
||||||
boolean hasKnownBindableProperties = hasKnownBindableProperties(name, context);
|
boolean hasKnownBindableProperties = hasKnownBindableProperties(name, context);
|
||||||
Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
|
Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
|
||||||
if (bean == null) {
|
if (bean == null) {
|
||||||
|
@ -56,8 +56,10 @@ class JavaBeanBinder implements BeanBinder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T create(Class<T> type, Context context) {
|
@SuppressWarnings("unchecked")
|
||||||
return BeanUtils.instantiateClass(type);
|
public <T> T create(Bindable<T> target, Context context) {
|
||||||
|
Class<T> type = (Class<T>) target.getType().resolve();
|
||||||
|
return (type != null) ? BeanUtils.instantiateClass(type) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
|
private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
|
||||||
|
@ -69,7 +71,7 @@ class JavaBeanBinder implements BeanBinder {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> boolean bind(BeanPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier) {
|
private <T> boolean bind(DataObjectPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier) {
|
||||||
boolean bound = false;
|
boolean bound = false;
|
||||||
for (BeanProperty beanProperty : bean.getProperties().values()) {
|
for (BeanProperty beanProperty : bean.getProperties().values()) {
|
||||||
bound |= bind(beanSupplier, propertyBinder, beanProperty);
|
bound |= bind(beanSupplier, propertyBinder, beanProperty);
|
||||||
|
@ -77,7 +79,8 @@ class JavaBeanBinder implements BeanBinder {
|
||||||
return bound;
|
return bound;
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> boolean bind(BeanSupplier<T> beanSupplier, BeanPropertyBinder propertyBinder, BeanProperty property) {
|
private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder,
|
||||||
|
BeanProperty property) {
|
||||||
String propertyName = property.getName();
|
String propertyName = property.getName();
|
||||||
ResolvableType type = property.getType();
|
ResolvableType type = property.getType();
|
||||||
Supplier<Object> value = property.getValue(beanSupplier);
|
Supplier<Object> value = property.getValue(beanSupplier);
|
||||||
|
@ -268,7 +271,7 @@ class JavaBeanBinder implements BeanBinder {
|
||||||
private Field field;
|
private Field field;
|
||||||
|
|
||||||
BeanProperty(String name, ResolvableType declaringClassType) {
|
BeanProperty(String name, ResolvableType declaringClassType) {
|
||||||
this.name = BeanPropertyName.toDashedForm(name);
|
this.name = DataObjectPropertyName.toDashedForm(name);
|
||||||
this.declaringClassType = declaringClassType;
|
this.declaringClassType = declaringClassType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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.List;
|
||||||
|
|
||||||
|
import kotlin.reflect.KFunction;
|
||||||
|
import kotlin.reflect.KParameter;
|
||||||
|
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||||
|
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||||
|
import org.springframework.core.KotlinDetector;
|
||||||
|
import org.springframework.core.ParameterNameDiscoverer;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataObjectBinder} for immutable value objects.
|
||||||
|
*
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Stephane Nicoll
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class ValueObjectBinder implements DataObjectBinder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Context context,
|
||||||
|
DataObjectPropertyBinder propertyBinder) {
|
||||||
|
ValueObject<T> valueObject = ValueObject.get(target);
|
||||||
|
if (valueObject == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<ConstructorParameter> parameters = valueObject.getConstructorParameters();
|
||||||
|
List<Object> args = new ArrayList<>(parameters.size());
|
||||||
|
boolean bound = false;
|
||||||
|
for (ConstructorParameter parameter : parameters) {
|
||||||
|
Object arg = parameter.bind(propertyBinder);
|
||||||
|
bound = bound || arg != null;
|
||||||
|
arg = (arg != null) ? arg : parameter.getDefaultValue(context.getConverter());
|
||||||
|
args.add(arg);
|
||||||
|
}
|
||||||
|
return bound ? valueObject.instantiate(args) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T create(Bindable<T> target, Binder.Context context) {
|
||||||
|
ValueObject<T> valueObject = ValueObject.get(target);
|
||||||
|
if (valueObject == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<ConstructorParameter> parameters = valueObject.getConstructorParameters();
|
||||||
|
List<Object> args = new ArrayList<>(parameters.size());
|
||||||
|
for (ConstructorParameter parameter : parameters) {
|
||||||
|
args.add(parameter.getDefaultValue(context.getConverter()));
|
||||||
|
}
|
||||||
|
return valueObject.instantiate(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value object being bound.
|
||||||
|
*
|
||||||
|
* @param <T> the value object type
|
||||||
|
*/
|
||||||
|
private abstract static class ValueObject<T> {
|
||||||
|
|
||||||
|
private final Constructor<T> constructor;
|
||||||
|
|
||||||
|
protected ValueObject(Constructor<T> constructor) {
|
||||||
|
this.constructor = constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T instantiate(List<Object> args) {
|
||||||
|
return BeanUtils.instantiateClass(this.constructor, args.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract List<ConstructorParameter> getConstructorParameters();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> ValueObject<T> get(Bindable<T> bindable) {
|
||||||
|
if (bindable.getValue() != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Class<T> type = (Class<T>) bindable.getType().resolve();
|
||||||
|
if (type == null || type.isEnum() || Modifier.isAbstract(type.getModifiers())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (KotlinDetector.isKotlinType(type)) {
|
||||||
|
return KotlinValueObject.get(type);
|
||||||
|
}
|
||||||
|
return DefaultValueObject.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ValueObject} implementation that is aware of Kotlin specific constructs.
|
||||||
|
*/
|
||||||
|
private static final class KotlinValueObject<T> extends ValueObject<T> {
|
||||||
|
|
||||||
|
private final List<ConstructorParameter> constructorParameters;
|
||||||
|
|
||||||
|
private KotlinValueObject(Constructor<T> primaryConstructor, KFunction<T> kotlinConstructor) {
|
||||||
|
super(primaryConstructor);
|
||||||
|
this.constructorParameters = parseConstructorParameters(kotlinConstructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ConstructorParameter> parseConstructorParameters(KFunction<T> kotlinConstructor) {
|
||||||
|
List<KParameter> parameters = kotlinConstructor.getParameters();
|
||||||
|
List<ConstructorParameter> result = new ArrayList<>(parameters.size());
|
||||||
|
for (KParameter parameter : parameters) {
|
||||||
|
String name = parameter.getName();
|
||||||
|
ResolvableType type = ResolvableType.forType(ReflectJvmMapping.getJavaType(parameter.getType()));
|
||||||
|
Annotation[] annotations = parameter.getAnnotations().toArray(new Annotation[0]);
|
||||||
|
result.add(new ConstructorParameter(name, type, annotations));
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ConstructorParameter> getConstructorParameters() {
|
||||||
|
return this.constructorParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ValueObject<T> get(Class<T> type) {
|
||||||
|
Constructor<T> primaryConstructor = BeanUtils.findPrimaryConstructor(type);
|
||||||
|
if (primaryConstructor == null || primaryConstructor.getParameterCount() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
KFunction<T> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(primaryConstructor);
|
||||||
|
if (kotlinConstructor != null) {
|
||||||
|
return new KotlinValueObject<>(primaryConstructor, kotlinConstructor);
|
||||||
|
}
|
||||||
|
return DefaultValueObject.get(primaryConstructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A default {@link ValueObject} implementation that uses only standard Java
|
||||||
|
* reflection calls.
|
||||||
|
*/
|
||||||
|
private static final class DefaultValueObject<T> extends ValueObject<T> {
|
||||||
|
|
||||||
|
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
|
||||||
|
|
||||||
|
private final List<ConstructorParameter> constructorParameters;
|
||||||
|
|
||||||
|
private DefaultValueObject(Constructor<T> constructor) {
|
||||||
|
super(constructor);
|
||||||
|
this.constructorParameters = parseConstructorParameters(constructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ConstructorParameter> parseConstructorParameters(Constructor<?> constructor) {
|
||||||
|
String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor);
|
||||||
|
Assert.state(names != null, () -> "Failed to extract parameter names for " + constructor);
|
||||||
|
Parameter[] parameters = constructor.getParameters();
|
||||||
|
List<ConstructorParameter> result = new ArrayList<>(parameters.length);
|
||||||
|
for (int i = 0; i < parameters.length; i++) {
|
||||||
|
String name = names[i];
|
||||||
|
ResolvableType type = ResolvableType.forConstructorParameter(constructor, i);
|
||||||
|
Annotation[] annotations = parameters[i].getDeclaredAnnotations();
|
||||||
|
result.add(new ConstructorParameter(name, type, annotations));
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ConstructorParameter> getConstructorParameters() {
|
||||||
|
return this.constructorParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T> ValueObject<T> get(Class<T> type) {
|
||||||
|
Constructor<?>[] constructors = type.getDeclaredConstructors();
|
||||||
|
return (constructors.length != 1) ? null : get((Constructor<T>) constructors[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static <T> DefaultValueObject<T> get(Constructor<T> constructor) {
|
||||||
|
if (constructor == null || constructor.getParameterCount() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new DefaultValueObject<>(constructor);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A constructor parameter being bound.
|
||||||
|
*/
|
||||||
|
private static class ConstructorParameter {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final ResolvableType type;
|
||||||
|
|
||||||
|
private final Annotation[] annotations;
|
||||||
|
|
||||||
|
ConstructorParameter(String name, ResolvableType type, Annotation[] annotations) {
|
||||||
|
this.name = DataObjectPropertyName.toDashedForm(name);
|
||||||
|
this.type = type;
|
||||||
|
this.annotations = annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getDefaultValue(BindConverter converter) {
|
||||||
|
for (Annotation annotation : this.annotations) {
|
||||||
|
if (annotation instanceof DefaultValue) {
|
||||||
|
return converter.convert(((DefaultValue) annotation).value(), this.type, this.annotations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object bind(DataObjectPropertyBinder propertyBinder) {
|
||||||
|
return propertyBinder.bindProperty(this.name, Bindable.of(this.type).withAnnotations(this.annotations));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -21,21 +21,21 @@ import org.junit.jupiter.api.Test;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link BeanPropertyName}.
|
* Tests for {@link DataObjectPropertyName}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
class BeanPropertyNameTests {
|
class DataObjectPropertyNameTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toDashedCaseShouldConvertValue() {
|
void toDashedCaseShouldConvertValue() {
|
||||||
assertThat(BeanPropertyName.toDashedForm("Foo")).isEqualTo("foo");
|
assertThat(DataObjectPropertyName.toDashedForm("Foo")).isEqualTo("foo");
|
||||||
assertThat(BeanPropertyName.toDashedForm("foo")).isEqualTo("foo");
|
assertThat(DataObjectPropertyName.toDashedForm("foo")).isEqualTo("foo");
|
||||||
assertThat(BeanPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar");
|
assertThat(DataObjectPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar");
|
||||||
assertThat(BeanPropertyName.toDashedForm("foo_bar")).isEqualTo("foo-bar");
|
assertThat(DataObjectPropertyName.toDashedForm("foo_bar")).isEqualTo("foo-bar");
|
||||||
assertThat(BeanPropertyName.toDashedForm("_foo_bar")).isEqualTo("-foo-bar");
|
assertThat(DataObjectPropertyName.toDashedForm("_foo_bar")).isEqualTo("-foo-bar");
|
||||||
assertThat(BeanPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar");
|
assertThat(DataObjectPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -30,11 +30,11 @@ import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link ConstructorParametersBinder}.
|
* Tests for {@link ValueObjectBinder}.
|
||||||
*
|
*
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
*/
|
*/
|
||||||
class ConstructorParametersBinderTests {
|
class ValueObjectBinderTests {
|
||||||
|
|
||||||
private final List<ConfigurationPropertySource> sources = new ArrayList<>();
|
private final List<ConfigurationPropertySource> sources = new ArrayList<>();
|
||||||
|
|
Loading…
Reference in New Issue