From 0b3015e4ffe7b12db4a44c7bf49e1081f7cb5d90 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 14 Jun 2019 15:29:40 -0700 Subject: [PATCH] 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. --- .../boot/context/properties/bind/Binder.java | 55 ++-- .../bind/ConstructorParametersBinder.java | 262 ------------------ ...{BeanBinder.java => DataObjectBinder.java} | 19 +- ...der.java => DataObjectPropertyBinder.java} | 6 +- ...yName.java => DataObjectPropertyName.java} | 19 +- .../properties/bind/JavaBeanBinder.java | 19 +- .../properties/bind/ValueObjectBinder.java | 240 ++++++++++++++++ ....java => DataObjectPropertyNameTests.java} | 16 +- ...Tests.java => ValueObjectBinderTests.java} | 4 +- 9 files changed, 308 insertions(+), 332 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/{BeanBinder.java => DataObjectBinder.java} (69%) rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/{BeanPropertyBinder.java => DataObjectPropertyBinder.java} (87%) rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/{BeanPropertyName.java => DataObjectPropertyName.java} (73%) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java rename spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/{BeanPropertyNameTests.java => DataObjectPropertyNameTests.java} (61%) rename spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/{ConstructorParametersBinderTests.java => ValueObjectBinderTests.java} (99%) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 4ca02da85b2..c9877e5a3ac 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -55,7 +55,7 @@ public class Binder { private static final Set> NON_BEAN_CLASSES = Collections .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 sources; @@ -282,7 +282,7 @@ public class Binder { result = context.getConverter().convert(result, target); } if (result == null && create) { - result = createBean(target, context); + result = create(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()); @@ -291,12 +291,11 @@ public class Binder { 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; + private Object create(Bindable target, Context context) { + for (DataObjectBinder dataObjectBinder : DATA_OBJECT_BINDERS) { + Object instance = dataObjectBinder.create(target, context); + if (instance != null) { + return instance; } } return null; @@ -331,15 +330,15 @@ public class Binder { return bindProperty(target, context, property); } catch (ConverterNotFoundException ex) { - // We might still be able to bind it as a bean - Object bean = bindBean(name, target, handler, context, allowRecursiveBinding); - if (bean != null) { - return bean; + // We might still be able to bind it using the recursive binders + Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding); + if (instance != null) { + return instance; } throw ex; } } - return bindBean(name, target, handler, context, allowRecursiveBinding); + return bindDataObject(name, target, handler, context, allowRecursiveBinding); } private AggregateBinder getAggregateBinder(Bindable target, Context context) { @@ -387,22 +386,22 @@ public class Binder { return result; } - private Object bindBean(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, - boolean allowRecursiveBinding) { + private Object bindDataObject(ConfigurationPropertyName name, Bindable target, BindHandler handler, + Context context, boolean allowRecursiveBinding) { if (isUnbindableBean(name, target, context)) { return null; } Class type = target.getType().resolve(Object.class); - if (!allowRecursiveBinding && context.hasBoundBean(type)) { + if (!allowRecursiveBinding && context.isBindingDataObject(type)) { return null; } - BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), + DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), propertyTarget, handler, context, false, false); - return context.withBean(type, () -> { - for (BeanBinder beanBinder : BEAN_BINDERS) { - Object bean = beanBinder.bind(name, target, context, propertyBinder); - if (bean != null) { - return bean; + return context.withDataObject(type, () -> { + for (DataObjectBinder dataObjectBinder : DATA_OBJECT_BINDERS) { + Object instance = dataObjectBinder.bind(name, target, context, propertyBinder); + if (instance != null) { + return instance; } } return null; @@ -457,7 +456,7 @@ public class Binder { private int sourcePushCount; - private final Deque> beans = new ArrayDeque<>(); + private final Deque> dataObjectBindings = new ArrayDeque<>(); private ConfigurationProperty configurationProperty; @@ -487,18 +486,18 @@ public class Binder { } } - private T withBean(Class bean, Supplier supplier) { - this.beans.push(bean); + private T withDataObject(Class type, Supplier supplier) { + this.dataObjectBindings.push(type); try { return withIncreasedDepth(supplier); } finally { - this.beans.pop(); + this.dataObjectBindings.pop(); } } - private boolean hasBoundBean(Class bean) { - return this.beans.contains(bean); + private boolean isBindingDataObject(Class type) { + return this.dataObjectBindings.contains(type); } private T withIncreasedDepth(Supplier supplier) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java deleted file mode 100644 index 504f677c3e6..00000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java +++ /dev/null @@ -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 bind(ConfigurationPropertyName name, Bindable target, Binder.Context context, - BeanPropertyBinder propertyBinder) { - Bean bean = Bean.get(target); - if (bean == null) { - return null; - } - List bound = bind(propertyBinder, bean, context.getConverter()); - return (bound != null) ? (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray()) : null; - } - - @Override - @SuppressWarnings("unchecked") - public T create(Class type, Binder.Context context) { - Bean bean = getBean(type); - if (bean == null) { - return null; - } - Collection parameters = bean.getParameters().values(); - List 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 Bean getBean(Class type) { - if (KOTLIN_PRESENT && KotlinDetector.isKotlinType(type)) { - return KotlinBeanProvider.get(type); - } - return SimpleBeanProvider.get(type); - } - - private List bind(BeanPropertyBinder propertyBinder, Bean bean, BindConverter converter) { - Collection parameters = bean.getParameters().values(); - List 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 parameters; - - private Bean(Constructor constructor, Map 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 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 parseParameters(KFunction constructor) { - Map 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 parseParameters(Constructor constructor) { - String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor); - Assert.state(parameterNames != null, () -> "Failed to extract parameter names for " + constructor); - Map 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; - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java similarity index 69% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java index 3db0ed22333..4bf63dd665d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java @@ -20,15 +20,18 @@ import org.springframework.boot.context.properties.bind.Binder.Context; 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 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}. * @param name the name being bound * @param target the bindable to bind @@ -37,15 +40,17 @@ interface BeanBinder { * @param the source type * @return a bound instance or {@code null} */ - T bind(ConfigurationPropertyName name, Bindable target, Context context, BeanPropertyBinder propertyBinder); + T bind(ConfigurationPropertyName name, Bindable target, Context context, + DataObjectPropertyBinder propertyBinder); /** - * Return a new instance for the specified type. - * @param type the type used for creating a new instance + * Return a newly created instance or {@code null} if the {@link DataObjectBinder} + * does not support the specified {@link Bindable}. + * @param target the bindable to create * @param context the bind context * @param the source type * @return the created instance */ - T create(Class type, Context context); + T create(Bindable target, Context context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectPropertyBinder.java similarity index 87% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectPropertyBinder.java index 774f8054f56..7ba5c8d02d5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectPropertyBinder.java @@ -17,13 +17,13 @@ package org.springframework.boot.context.properties.bind; /** - * Binder that can be used by {@link BeanBinder} implementations to recursively bind bean - * properties. + * Binder that can be used by {@link DataObjectBinder} implementations to bind the data + * object properties. * * @author Phillip Webb * @author Madhura Bhave */ -interface BeanPropertyBinder { +interface DataObjectPropertyBinder { /** * Bind the given property. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectPropertyName.java similarity index 73% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectPropertyName.java index 343f74a2f11..c0384347043 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanPropertyName.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectPropertyName.java @@ -17,14 +17,15 @@ 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 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 */ 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(); 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); if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') { result.append('-'); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index 7565d800354..90e55d4acb5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -35,16 +35,16 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; /** - * {@link BeanBinder} for mutable Java Beans. + * {@link DataObjectBinder} for mutable Java Beans. * * @author Phillip Webb * @author Madhura Bhave */ -class JavaBeanBinder implements BeanBinder { +class JavaBeanBinder implements DataObjectBinder { @Override public T bind(ConfigurationPropertyName name, Bindable target, Context context, - BeanPropertyBinder propertyBinder) { + DataObjectPropertyBinder propertyBinder) { boolean hasKnownBindableProperties = hasKnownBindableProperties(name, context); Bean bean = Bean.get(target, hasKnownBindableProperties); if (bean == null) { @@ -56,8 +56,10 @@ class JavaBeanBinder implements BeanBinder { } @Override - public T create(Class type, Context context) { - return BeanUtils.instantiateClass(type); + @SuppressWarnings("unchecked") + public T create(Bindable target, Context context) { + Class type = (Class) target.getType().resolve(); + return (type != null) ? BeanUtils.instantiateClass(type) : null; } private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) { @@ -69,7 +71,7 @@ class JavaBeanBinder implements BeanBinder { return false; } - private boolean bind(BeanPropertyBinder propertyBinder, Bean bean, BeanSupplier beanSupplier) { + private boolean bind(DataObjectPropertyBinder propertyBinder, Bean bean, BeanSupplier beanSupplier) { boolean bound = false; for (BeanProperty beanProperty : bean.getProperties().values()) { bound |= bind(beanSupplier, propertyBinder, beanProperty); @@ -77,7 +79,8 @@ class JavaBeanBinder implements BeanBinder { return bound; } - private boolean bind(BeanSupplier beanSupplier, BeanPropertyBinder propertyBinder, BeanProperty property) { + private boolean bind(BeanSupplier beanSupplier, DataObjectPropertyBinder propertyBinder, + BeanProperty property) { String propertyName = property.getName(); ResolvableType type = property.getType(); Supplier value = property.getValue(beanSupplier); @@ -268,7 +271,7 @@ class JavaBeanBinder implements BeanBinder { private Field field; BeanProperty(String name, ResolvableType declaringClassType) { - this.name = BeanPropertyName.toDashedForm(name); + this.name = DataObjectPropertyName.toDashedForm(name); this.declaringClassType = declaringClassType; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java new file mode 100644 index 00000000000..7fdb4b53d46 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -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 bind(ConfigurationPropertyName name, Bindable target, Binder.Context context, + DataObjectPropertyBinder propertyBinder) { + ValueObject valueObject = ValueObject.get(target); + if (valueObject == null) { + return null; + } + List parameters = valueObject.getConstructorParameters(); + List 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 create(Bindable target, Binder.Context context) { + ValueObject valueObject = ValueObject.get(target); + if (valueObject == null) { + return null; + } + List parameters = valueObject.getConstructorParameters(); + List 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 the value object type + */ + private abstract static class ValueObject { + + private final Constructor constructor; + + protected ValueObject(Constructor constructor) { + this.constructor = constructor; + } + + public T instantiate(List args) { + return BeanUtils.instantiateClass(this.constructor, args.toArray()); + } + + public abstract List getConstructorParameters(); + + @SuppressWarnings("unchecked") + public static ValueObject get(Bindable bindable) { + if (bindable.getValue() != null) { + return null; + } + Class type = (Class) 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 extends ValueObject { + + private final List constructorParameters; + + private KotlinValueObject(Constructor primaryConstructor, KFunction kotlinConstructor) { + super(primaryConstructor); + this.constructorParameters = parseConstructorParameters(kotlinConstructor); + } + + private List parseConstructorParameters(KFunction kotlinConstructor) { + List parameters = kotlinConstructor.getParameters(); + List 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 getConstructorParameters() { + return this.constructorParameters; + } + + public static ValueObject get(Class type) { + Constructor primaryConstructor = BeanUtils.findPrimaryConstructor(type); + if (primaryConstructor == null || primaryConstructor.getParameterCount() == 0) { + return null; + } + KFunction 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 extends ValueObject { + + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private final List constructorParameters; + + private DefaultValueObject(Constructor constructor) { + super(constructor); + this.constructorParameters = parseConstructorParameters(constructor); + } + + private static List 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 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 getConstructorParameters() { + return this.constructorParameters; + } + + @SuppressWarnings("unchecked") + static ValueObject get(Class type) { + Constructor[] constructors = type.getDeclaredConstructors(); + return (constructors.length != 1) ? null : get((Constructor) constructors[0]); + } + + static DefaultValueObject get(Constructor 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)); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DataObjectPropertyNameTests.java similarity index 61% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DataObjectPropertyNameTests.java index 398241ae761..dcdeeebf4b6 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BeanPropertyNameTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DataObjectPropertyNameTests.java @@ -21,21 +21,21 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link BeanPropertyName}. + * Tests for {@link DataObjectPropertyName}. * * @author Phillip Webb * @author Madhura Bhave */ -class BeanPropertyNameTests { +class DataObjectPropertyNameTests { @Test void toDashedCaseShouldConvertValue() { - assertThat(BeanPropertyName.toDashedForm("Foo")).isEqualTo("foo"); - assertThat(BeanPropertyName.toDashedForm("foo")).isEqualTo("foo"); - assertThat(BeanPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar"); - assertThat(BeanPropertyName.toDashedForm("foo_bar")).isEqualTo("foo-bar"); - assertThat(BeanPropertyName.toDashedForm("_foo_bar")).isEqualTo("-foo-bar"); - assertThat(BeanPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar"); + assertThat(DataObjectPropertyName.toDashedForm("Foo")).isEqualTo("foo"); + assertThat(DataObjectPropertyName.toDashedForm("foo")).isEqualTo("foo"); + assertThat(DataObjectPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar"); + assertThat(DataObjectPropertyName.toDashedForm("foo_bar")).isEqualTo("foo-bar"); + assertThat(DataObjectPropertyName.toDashedForm("_foo_bar")).isEqualTo("-foo-bar"); + assertThat(DataObjectPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar"); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java similarity index 99% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index b2c7241d004..322ae2d54df 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -30,11 +30,11 @@ import org.springframework.format.annotation.DateTimeFormat; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link ConstructorParametersBinder}. + * Tests for {@link ValueObjectBinder}. * * @author Madhura Bhave */ -class ConstructorParametersBinderTests { +class ValueObjectBinderTests { private final List sources = new ArrayList<>();