package org.springframework.data.web;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import org.springframework.beans.AbstractPropertyAccessor;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.ConfigurablePropertyAccessor;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.PropertyAccessor;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.CollectionFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.bind.WebDataBinder;
class MapDataBinder extends WebDataBinder {
private final Class<?> type;
private final ConversionService conversionService;
public MapDataBinder(Class<?> type, ConversionService conversionService) {
super(new HashMap<String, Object>());
this.type = type;
this.conversionService = conversionService;
}
@Nonnull
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> getTarget() {
Object target = super.getTarget();
if (target == null) {
throw new IllegalStateException("Target bean should never be null!");
}
return (Map<String, Object>) target;
}
@Override
protected ConfigurablePropertyAccessor getPropertyAccessor() {
return new MapPropertyAccessor(type, getTarget(), conversionService);
}
@RequiredArgsConstructor
private static class MapPropertyAccessor extends AbstractPropertyAccessor {
private static final SpelExpressionParser PARSER = new SpelExpressionParser(
new SpelParserConfiguration(false, true));
private final @NonNull Class<?> type;
private final @NonNull Map<String, Object> map;
private final @NonNull ConversionService conversionService;
@Override
public boolean isReadableProperty(String propertyName) {
throw new UnsupportedOperationException();
}
@Override
public boolean isWritableProperty(String propertyName) {
try {
return getPropertyPath(propertyName) != null;
} catch (PropertyReferenceException o_O) {
return false;
}
}
@Nullable
@Override
public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
throw new UnsupportedOperationException();
}
@Nullable
@Override
public Object getPropertyValue(String propertyName) throws BeansException {
throw new UnsupportedOperationException();
}
@Override
public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
if (!isWritableProperty(propertyName)) {
throw new NotWritablePropertyException(type, propertyName);
}
PropertyPath leafProperty = getPropertyPath(propertyName).getLeafProperty();
TypeInformation<?> owningType = leafProperty.getOwningType();
TypeInformation<?> propertyType = leafProperty.getTypeInformation();
propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType;
if (propertyType != null && conversionRequired(value, propertyType.getType())) {
PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(owningType.getType(),
leafProperty.getSegment());
if (descriptor == null) {
throw new IllegalStateException(String.format("Couldn't find PropertyDescriptor for %s on %s!",
leafProperty.getSegment(), owningType.getType()));
}
MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1);
TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0);
if (typeDescriptor == null) {
throw new IllegalStateException(
String.format("Couldn't obtain type descriptor for method parameter %s!", methodParameter));
}
value = conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
}
EvaluationContext context = SimpleEvaluationContext
.forPropertyAccessors(new PropertyTraversingMapAccessor(type, conversionService))
.withConversionService(conversionService)
.withRootObject(map)
.build();
Expression expression = PARSER.parseExpression(propertyName);
try {
expression.setValue(context, value);
} catch (SpelEvaluationException o_O) {
throw new NotWritablePropertyException(type, propertyName, "Could not write property!", o_O);
}
}
private boolean conversionRequired(@Nullable Object source, Class<?> targetType) {
if (source == null || targetType.isInstance(source)) {
return false;
}
return conversionService.canConvert(source.getClass(), targetType);
}
private PropertyPath getPropertyPath(String propertyName) {
String plainPropertyPath = propertyName.replaceAll("\\[.*?\\]", "");
return PropertyPath.from(plainPropertyPath, type);
}
private static final class PropertyTraversingMapAccessor extends MapAccessor {
private final ConversionService conversionService;
private Class<?> type;
public PropertyTraversingMapAccessor(Class<?> type, ConversionService conversionService) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(conversionService, "ConversionService must not be null!");
this.type = type;
this.conversionService = conversionService;
}
@Override
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
return true;
}
@Override
@SuppressWarnings("unchecked")
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
if (target == null) {
return TypedValue.NULL;
}
PropertyPath path = PropertyPath.from(name, type);
try {
return super.read(context, target, name);
} catch (AccessException o_O) {
Object emptyResult = path.isCollection() ? CollectionFactory.createCollection(List.class, 0)
: CollectionFactory.createMap(Map.class, 0);
((Map<String, Object>) target).put(name, emptyResult);
return new TypedValue(emptyResult, getDescriptor(path, emptyResult));
} finally {
this.type = path.getType();
}
}
private TypeDescriptor getDescriptor(PropertyPath path, Object emptyValue) {
Class<?> actualPropertyType = path.getType();
TypeDescriptor valueDescriptor = conversionService.canConvert(String.class, actualPropertyType)
? TypeDescriptor.valueOf(String.class)
: TypeDescriptor.valueOf(HashMap.class);
return path.isCollection() ? TypeDescriptor.collection(emptyValue.getClass(), valueDescriptor)
: TypeDescriptor.map(emptyValue.getClass(), TypeDescriptor.valueOf(String.class), valueDescriptor);
}
}
}
}