/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.context.properties.bind;
import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyState;
import org.springframework.core.ResolvableType;
/**
* {@link BeanBinder} for mutable Java Beans.
*
* @author Phillip Webb
* @author Madhura Bhave
*/
class JavaBeanBinder implements BeanBinder {
@Override
public <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
BindContext context, BeanPropertyBinder propertyBinder) {
boolean hasKnownBindableProperties = context.streamSources().anyMatch((
s) -> s.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT);
Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
if (bean == null) {
return null;
}
BeanSupplier<T> beanSupplier = bean.getSupplier(target);
boolean bound = bind(propertyBinder, bean, beanSupplier);
return (bound ? beanSupplier.get() : null);
}
private <T> boolean bind(BeanPropertyBinder propertyBinder, Bean<T> bean,
BeanSupplier<T> beanSupplier) {
boolean bound = false;
for (Map.Entry<String, BeanProperty> entry : bean.getProperties().entrySet()) {
bound |= bind(beanSupplier, propertyBinder, entry.getValue());
}
return bound;
}
private <T> boolean bind(BeanSupplier<T> beanSupplier,
BeanPropertyBinder propertyBinder, BeanProperty property) {
String propertyName = property.getName();
ResolvableType type = property.getType();
Supplier<Object> value = property.getValue(beanSupplier);
Annotation[] annotations = property.getAnnotations();
Object bound = propertyBinder.bindProperty(propertyName,
Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
if (bound == null) {
return false;
}
if (property.isSettable()) {
property.setValue(beanSupplier, bound);
}
else if (value == null || !bound.equals(value.get())) {
throw new IllegalStateException(
"No setter found for property: " + property.getName());
}
return true;
}
/**
* The bean being bound.
*/
private static class Bean<T> {
private static Bean<?> cached;
private final Class<?> type;
private final Map<String, BeanProperty> properties = new LinkedHashMap<>();
Bean(Class<?> type) {
this.type = type;
putProperties(type);
}
private void putProperties(Class<?> type) {
while (type != null && !Object.class.equals(type)) {
for (Method method : type.getDeclaredMethods()) {
if (isCandidate(method)) {
addMethod(method);
}
}
for (Field field : type.getDeclaredFields()) {
addField(field);
}
type = type.getSuperclass();
}
}
private boolean isCandidate(Method method) {
return Modifier.isPublic(method.getModifiers())
&& !Object.class.equals(method.getDeclaringClass())
&& !Class.class.equals(method.getDeclaringClass());
}
private void addMethod(Method method) {
String name = method.getName();
int parameterCount = method.getParameterCount();
if (name.startsWith("get") && parameterCount == 0) {
name = Introspector.decapitalize(name.substring(3));
this.properties.computeIfAbsent(name, BeanProperty::new)
.addGetter(method);
}
else if (name.startsWith("is") && parameterCount == 0) {
name = Introspector.decapitalize(name.substring(2));
this.properties.computeIfAbsent(name, BeanProperty::new)
.addGetter(method);
}
else if (name.startsWith("set") && parameterCount == 1) {
name = Introspector.decapitalize(name.substring(3));
this.properties.computeIfAbsent(name, BeanProperty::new)
.addSetter(method);
}
}
private void addField(Field field) {
BeanProperty property = this.properties.get(field.getName());
if (property != null) {
property.addField(field);
}
}
public Class<?> getType() {
return this.type;
}
public Map<String, BeanProperty> getProperties() {
return this.properties;
}
@SuppressWarnings("unchecked")
public BeanSupplier<T> getSupplier(Bindable<T> target) {
return new BeanSupplier<>(() -> {
T instance = null;
if (target.getValue() != null) {
instance = target.getValue().get();
}
if (instance == null) {
instance = (T) BeanUtils.instantiateClass(this.type);
}
return instance;
});
}
@SuppressWarnings("unchecked")
public static <T> Bean<T> get(Bindable<T> bindable,
boolean useExistingValueForType) {
Class<?> type = bindable.getType().resolve();
Supplier<T> value = bindable.getValue();
if (value == null && !isInstantiable(type)) {
return null;
}
if (useExistingValueForType && value != null) {
T instance = value.get();
type = (instance != null ? instance.getClass() : type);
}
Bean<?> bean = Bean.cached;
if (bean == null || !type.equals(bean.getType())) {
bean = new Bean<>(type);
cached = bean;
}
return (Bean<T>) bean;
}
private static boolean isInstantiable(Class<?> type) {
if (type.isInterface()) {
return false;
}
try {
type.getDeclaredConstructor();
return true;
}
catch (Exception ex) {
return false;
}
}
}
private static class BeanSupplier<T> implements Supplier<T> {
private final Supplier<T> factory;
private T instance;
BeanSupplier(Supplier<T> factory) {
this.factory = factory;
}
@Override
public T get() {
if (this.instance == null) {
this.instance = this.factory.get();
}
return this.instance;
}
}
/**
* A bean property being bound.
*/
private static class BeanProperty {
private final String name;
private Method getter;
private Method setter;
private Field field;
BeanProperty(String name) {
this.name = BeanPropertyName.toDashedForm(name);
}
public void addGetter(Method getter) {
if (this.getter == null) {
this.getter = getter;
}
}
public void addSetter(Method setter) {
if (this.setter == null) {
this.setter = setter;
}
}
public void addField(Field field) {
if (this.field == null) {
this.field = field;
}
}
public String getName() {
return this.name;
}
public ResolvableType getType() {
if (this.setter != null) {
return ResolvableType.forMethodParameter(this.setter, 0);
}
return ResolvableType.forMethodReturnType(this.getter);
}
public Annotation[] getAnnotations() {
try {
return (this.field == null ? null : this.field.getDeclaredAnnotations());
}
catch (Exception ex) {
return null;
}
}
public Supplier<Object> getValue(Supplier<?> instance) {
if (this.getter == null) {
return null;
}
return () -> {
try {
this.getter.setAccessible(true);
return this.getter.invoke(instance.get());
}
catch (Exception ex) {
throw new IllegalStateException(
"Unable to get value for property " + this.name, ex);
}
};
}
public boolean isSettable() {
return this.setter != null;
}
public void setValue(Supplier<?> instance, Object value) {
try {
this.setter.setAccessible(true);
this.setter.invoke(instance.get(), value);
}
catch (Exception ex) {
throw new IllegalStateException(
"Unable to set value for property " + this.name, ex);
}
}
}
}