org.springframework.data.web.MapDataBinder Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015-2024 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.data.web;
import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.bind.WebDataBinder;
/**
* A {@link WebDataBinder} that automatically binds all properties exposed in the given type using a {@link Map}.
*
* @author Oliver Gierke
* @author Johannes Englmeier
* @since 1.10
*/
class MapDataBinder extends WebDataBinder {
private final Class> type;
private final ConversionService conversionService;
/**
* Creates a new {@link MapDataBinder} for the given type and {@link ConversionService}.
*
* @param type target type to detect property that need to be bound.
* @param conversionService the {@link ConversionService} to be used to preprocess values.
*/
public MapDataBinder(Class> type, ConversionService conversionService) {
super(new HashMap());
this.type = type;
this.conversionService = conversionService;
}
@NonNull
@Override
@SuppressWarnings("unchecked")
public Map getTarget() {
Object target = super.getTarget();
if (target == null) {
throw new IllegalStateException("Target bean should never be null");
}
return (Map) target;
}
@Override
protected ConfigurablePropertyAccessor getPropertyAccessor() {
return new MapPropertyAccessor(type, getTarget(), conversionService);
}
/**
* {@link PropertyAccessor} to store and retrieve values in a {@link Map}. Uses Spring Expression language to create
* deeply nested Map structures.
*
* @author Oliver Gierke
* @author Johannes Englmeier
* @since 1.10
*/
private static class MapPropertyAccessor extends AbstractPropertyAccessor {
private static final SpelExpressionParser PARSER = new SpelExpressionParser(
new SpelParserConfiguration(false, true));
private final Class> type;
private final Map map;
private final ConversionService conversionService;
public MapPropertyAccessor(Class> type, Map map, ConversionService conversionService) {
this.type = type;
this.map = map;
this.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);
}
/**
* A special {@link MapAccessor} that traverses properties on the configured type to automatically create nested Map
* and collection values as necessary.
*
* @author Oliver Gierke
* @since 1.10
*/
private static final class PropertyTraversingMapAccessor extends MapAccessor {
private final ConversionService conversionService;
private Class> type;
/**
* Creates a new {@link PropertyTraversingMapAccessor} for the given type and {@link ConversionService}.
*
* @param type must not be {@literal null}.
* @param conversionService must not be {@literal null}.
*/
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) target).put(name, emptyResult);
return new TypedValue(emptyResult, getDescriptor(path, emptyResult));
} finally {
this.type = path.getType();
}
}
/**
* Returns the type descriptor for the given {@link PropertyPath} and empty value for that path.
*
* @param path must not be {@literal null}.
* @param emptyValue must not be {@literal null}.
* @return
*/
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);
}
}
}
}