All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.data.web.MapDataBinder Maven / Gradle / Ivy

/*
 * Copyright 2015 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.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.core.convert.support.DefaultConversionService;
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.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
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
 * @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;
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.validation.DataBinder#getTarget()
	 */
	@Override
	@SuppressWarnings("unchecked")
	public Map getTarget() {
		return (Map) super.getTarget();
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.validation.DataBinder#getPropertyAccessor()
	 */
	@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
	 * @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;

		/**
		 * Creates a new {@link MapPropertyAccessor} for the given type, map and {@link ConversionService}.
		 * 
		 * @param type must not be {@literal null}.
		 * @param map must not be {@literal null}.
		 * @param conversionService must not be {@literal null}.
		 */
		public MapPropertyAccessor(Class type, Map map, ConversionService conversionService) {

			Assert.notNull(type, "Type must not be null!");
			Assert.notNull(map, "Map must not be null!");
			Assert.notNull(conversionService, "ConversionService must not be null!");

			this.type = type;
			this.map = map;
			this.conversionService = conversionService;
		}

		/* 
		 * (non-Javadoc)
		 * @see org.springframework.beans.PropertyAccessor#isReadableProperty(java.lang.String)
		 */
		@Override
		public boolean isReadableProperty(String propertyName) {
			throw new UnsupportedOperationException();
		}

		/* 
		 * (non-Javadoc)
		 * @see org.springframework.beans.PropertyAccessor#isWritableProperty(java.lang.String)
		 */
		@Override
		public boolean isWritableProperty(String propertyName) {

			try {
				return getPropertyPath(propertyName) != null;
			} catch (PropertyReferenceException o_O) {
				return false;
			}
		}

		/* 
		 * (non-Javadoc)
		 * @see org.springframework.beans.PropertyAccessor#getPropertyTypeDescriptor(java.lang.String)
		 */
		@Override
		public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
			throw new UnsupportedOperationException();
		}

		/* 
		 * (non-Javadoc)
		 * @see org.springframework.beans.AbstractPropertyAccessor#getPropertyValue(java.lang.String)
		 */
		@Override
		public Object getPropertyValue(String propertyName) throws BeansException {
			throw new UnsupportedOperationException();
		}

		/* 
		 * (non-Javadoc)
		 * @see org.springframework.beans.AbstractPropertyAccessor#setPropertyValue(java.lang.String, java.lang.Object)
		 */
		@Override
		public void setPropertyValue(String propertyName, Object value) throws BeansException {

			if (!isWritableProperty(propertyName)) {
				throw new NotWritablePropertyException(type, propertyName);
			}

			StandardEvaluationContext context = new StandardEvaluationContext();
			context.addPropertyAccessor(new PropertyTraversingMapAccessor(type, new DefaultConversionService()));
			context.setTypeConverter(new StandardTypeConverter(conversionService));
			context.setRootObject(map);

			Expression expression = PARSER.parseExpression(propertyName);

			PropertyPath leafProperty = getPropertyPath(propertyName).getLeafProperty();
			TypeInformation owningType = leafProperty.getOwningType();
			TypeInformation propertyType = owningType.getProperty(leafProperty.getSegment());

			propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType;

			if (conversionRequired(value, propertyType.getType())) {

				PropertyDescriptor descriptor = BeanUtils
						.getPropertyDescriptor(owningType.getType(), leafProperty.getSegment());
				MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1);
				TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0);

				value = conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
			}

			expression.setValue(context, value);
		}

		private boolean conversionRequired(Object source, Class targetType) {

			if (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;
			}

			/*
			 * (non-Javadoc)
			 * @see org.springframework.context.expression.MapAccessor#canRead(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
			 */
			@Override
			public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
				return true;
			}

			/* 
			 * (non-Javadoc)
			 * @see org.springframework.context.expression.MapAccessor#read(org.springframework.expression.EvaluationContext, java.lang.Object, java.lang.String)
			 */
			@Override
			@SuppressWarnings("unchecked")
			public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {

				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);

			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy