spring-batch BeanWrapperFieldSetMapper 源码

  • 2022-08-16
  • 浏览 (432)

spring-batch BeanWrapperFieldSetMapper 代码

文件路径:/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java

/*
 * Copyright 2006-2021 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.batch.item.file.mapping;

import java.beans.PropertyEditor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.batch.support.DefaultPropertyEditorRegistrar;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.CustomEditorConfigurer;
import org.springframework.core.convert.ConversionService;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;

/**
 * {@link FieldSetMapper} implementation based on bean property paths. The
 * {@link FieldSet} to be mapped should have field name meta data corresponding to bean
 * property paths in an instance of the desired type. The instance is created and
 * initialized either by referring to a prototype object by bean name in the enclosing
 * BeanFactory, or by providing a class to instantiate reflectively.<br>
 * <br>
 *
 * Nested property paths, including indexed properties in maps and collections, can be
 * referenced by the {@link FieldSet} names. They will be converted to nested bean
 * properties inside the prototype. The {@link FieldSet} and the prototype are thus
 * tightly coupled by the fields that are available and those that can be initialized. If
 * some of the nested properties are optional (e.g. collection members) they need to be
 * removed by a post processor.<br>
 * <br>
 *
 * To customize the way that {@link FieldSet} values are converted to the desired type for
 * injecting into the prototype there are several choices. You can inject
 * {@link PropertyEditor} instances directly through the {@link #setCustomEditors(Map)
 * customEditors} property, or you can override the {@link #createBinder(Object)} and
 * {@link #initBinder(DataBinder)} methods, or you can provide a custom {@link FieldSet}
 * implementation. You can also use a {@link ConversionService} to convert to the desired
 * type through the {@link #setConversionService(ConversionService) conversionService}
 * property. <br>
 * <br>
 *
 * Property name matching is "fuzzy" in the sense that it tolerates close matches, as long
 * as the match is unique. For instance:
 *
 * <ul>
 * <li>Quantity = quantity (field names can be capitalised)</li>
 * <li>ISIN = isin (acronyms can be lower case bean property names, as per Java Beans
 * recommendations)</li>
 * <li>DuckPate = duckPate (capitalisation including camel casing)</li>
 * <li>ITEM_ID = itemId (capitalisation and replacing word boundary with underscore)</li>
 * <li>ORDER.CUSTOMER_ID = order.customerId (nested paths are recursively checked)</li>
 * </ul>
 *
 * The algorithm used to match a property name is to start with an exact match and then
 * search successively through more distant matches until precisely one match is found. If
 * more than one match is found there will be an error.
 *
 * @author Dave Syer
 * @author Mahmoud Ben Hassine
 *
 */
public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar
		implements FieldSetMapper<T>, BeanFactoryAware, InitializingBean {

	private String name;

	private Class<? extends T> type;

	private BeanFactory beanFactory;

	private ConcurrentMap<DistanceHolder, ConcurrentMap<String, String>> propertiesMatched = new ConcurrentHashMap<>();

	private int distanceLimit = 5;

	private boolean strict = true;

	private ConversionService conversionService;

	private boolean isCustomEditorsSet;

	/*
	 * (non-Javadoc)
	 *
	 * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org
	 * .springframework.beans.factory.BeanFactory)
	 */
	@Override
	public void setBeanFactory(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
	}

	/**
	 * The maximum difference that can be tolerated in spelling between input key names
	 * and bean property names. Defaults to 5, but could be set lower if the field names
	 * match the bean names.
	 * @param distanceLimit the distance limit to set
	 */
	public void setDistanceLimit(int distanceLimit) {
		this.distanceLimit = distanceLimit;
	}

	/**
	 * The bean name (id) for an object that can be populated from the field set that will
	 * be passed into {@link #mapFieldSet(FieldSet)}. Typically a prototype scoped bean so
	 * that a new instance is returned for each field set mapped.
	 *
	 * Either this property or the type property must be specified, but not both.
	 * @param name the name of a prototype bean in the enclosing BeanFactory
	 */
	public void setPrototypeBeanName(String name) {
		this.name = name;
	}

	/**
	 * Public setter for the type of bean to create instead of using a prototype bean. An
	 * object of this type will be created from its default constructor for every call to
	 * {@link #mapFieldSet(FieldSet)}.<br>
	 *
	 * Either this property or the prototype bean name must be specified, but not both.
	 * @param type the type to set
	 */
	public void setTargetType(Class<? extends T> type) {
		this.type = type;
	}

	/**
	 * Check that precisely one of type or prototype bean name is specified.
	 * @throws IllegalStateException if neither is set or both properties are set.
	 *
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		Assert.state(name != null || type != null, "Either name or type must be provided.");
		Assert.state(name == null || type == null, "Both name and type cannot be specified together.");
		Assert.state(!this.isCustomEditorsSet || this.conversionService == null,
				"Both customEditor and conversionService cannot be specified together.");
	}

	/**
	 * Map the {@link FieldSet} to an object retrieved from the enclosing Spring context,
	 * or to a new instance of the required type if no prototype is available.
	 * @throws BindException if there is a type conversion or other error (if the
	 * {@link DataBinder} from {@link #createBinder(Object)} has errors after binding).
	 * @throws NotWritablePropertyException if the {@link FieldSet} contains a field that
	 * cannot be mapped to a bean property.
	 * @see org.springframework.batch.item.file.mapping.FieldSetMapper#mapFieldSet(FieldSet)
	 */
	@Override
	public T mapFieldSet(FieldSet fs) throws BindException {
		T copy = getBean();
		DataBinder binder = createBinder(copy);
		binder.bind(new MutablePropertyValues(getBeanProperties(copy, fs.getProperties())));
		if (binder.getBindingResult().hasErrors()) {
			throw new BindException(binder.getBindingResult());
		}
		return copy;
	}

	/**
	 * Create a binder for the target object. The binder will then be used to bind the
	 * properties form a field set into the target object. This implementation creates a
	 * new {@link DataBinder} and calls out to {@link #initBinder(DataBinder)} and
	 * {@link #registerCustomEditors(PropertyEditorRegistry)}.
	 * @param target Object to bind to
	 * @return a {@link DataBinder} that can be used to bind properties to the target.
	 */
	protected DataBinder createBinder(Object target) {
		DataBinder binder = new DataBinder(target);
		binder.setIgnoreUnknownFields(!this.strict);
		initBinder(binder);
		registerCustomEditors(binder);
		if (this.conversionService != null) {
			binder.setConversionService(this.conversionService);
		}
		return binder;
	}

	/**
	 * Initialize a new binder instance. This hook allows customization of binder settings
	 * such as the {@link DataBinder#initDirectFieldAccess() direct field access}. Called
	 * by {@link #createBinder(Object)}.
	 * <p>
	 * Note that registration of custom property editors can be done in
	 * {@link #registerCustomEditors(PropertyEditorRegistry)}.
	 * </p>
	 * @param binder new binder instance
	 * @see #createBinder(Object)
	 */
	protected void initBinder(DataBinder binder) {
	}

	@SuppressWarnings("unchecked")
	private T getBean() {
		if (name != null) {
			return (T) beanFactory.getBean(name);
		}
		try {
			return type.getDeclaredConstructor().newInstance();
		}
		catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
			ReflectionUtils.handleReflectionException(e);
		}
		// should not happen
		throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
	}

	/**
	 * @param bean Object to get properties for
	 * @param properties Properties to retrieve
	 */
	private Properties getBeanProperties(Object bean, Properties properties) {

		if (this.distanceLimit == 0) {
			return properties;
		}

		Class<?> cls = bean.getClass();

		// Map from field names to property names
		DistanceHolder distanceKey = new DistanceHolder(cls, distanceLimit);
		if (!propertiesMatched.containsKey(distanceKey)) {
			propertiesMatched.putIfAbsent(distanceKey, new ConcurrentHashMap<>());
		}
		Map<String, String> matches = new HashMap<>(propertiesMatched.get(distanceKey));

		@SuppressWarnings({ "unchecked", "rawtypes" })
		Set<String> keys = new HashSet(properties.keySet());
		for (String key : keys) {

			if (matches.containsKey(key)) {
				switchPropertyNames(properties, key, matches.get(key));
				continue;
			}

			String name = findPropertyName(bean, key);

			if (name != null) {
				if (matches.containsValue(name)) {
					throw new NotWritablePropertyException(cls, name, "Duplicate match with distance <= "
							+ distanceLimit + " found for this property in input keys: " + keys
							+ ". (Consider reducing the distance limit or changing the input key names to get a closer match.)");
				}
				matches.put(key, name);
				switchPropertyNames(properties, key, name);
			}
		}

		propertiesMatched.replace(distanceKey, new ConcurrentHashMap<>(matches));
		return properties;
	}

	private String findPropertyName(Object bean, String key) {

		if (bean == null) {
			return null;
		}

		Class<?> cls = bean.getClass();

		int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(key);
		String prefix;
		String suffix;

		// If the property name is nested recurse down through the properties
		// looking for a match.
		if (index > 0) {
			prefix = key.substring(0, index);
			suffix = key.substring(index + 1, key.length());
			String nestedName = findPropertyName(bean, prefix);
			if (nestedName == null) {
				return null;
			}

			Object nestedValue = getPropertyValue(bean, nestedName);
			String nestedPropertyName = findPropertyName(nestedValue, suffix);
			return nestedPropertyName == null ? null : nestedName + "." + nestedPropertyName;
		}

		String name = null;
		int distance = 0;
		index = key.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);

		if (index > 0) {
			prefix = key.substring(0, index);
			suffix = key.substring(index);
		}
		else {
			prefix = key;
			suffix = "";
		}

		while (name == null && distance <= distanceLimit) {
			String[] candidates = PropertyMatches.forProperty(prefix, cls, distance).getPossibleMatches();
			// If we find precisely one match, then use that one...
			if (candidates.length == 1) {
				String candidate = candidates[0];
				if (candidate.equals(prefix)) { // if it's the same don't
					// replace it...
					name = key;
				}
				else {
					name = candidate + suffix;
				}
			}
			distance++;
		}
		return name;
	}

	private Object getPropertyValue(Object bean, String nestedName) {
		BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
		wrapper.setAutoGrowNestedPaths(true);

		Object nestedValue = wrapper.getPropertyValue(nestedName);
		if (nestedValue == null) {
			try {
				nestedValue = wrapper.getPropertyType(nestedName).getDeclaredConstructor().newInstance();
				wrapper.setPropertyValue(nestedName, nestedValue);
			}
			catch (InstantiationException | IllegalAccessException | NoSuchMethodException
					| InvocationTargetException e) {
				ReflectionUtils.handleReflectionException(e);
			}
		}
		return nestedValue;
	}

	private void switchPropertyNames(Properties properties, String oldName, String newName) {
		String value = properties.getProperty(oldName);
		properties.remove(oldName);
		properties.setProperty(newName, value);
	}

	/**
	 * Public setter for the 'strict' property. If true, then
	 * {@link #mapFieldSet(FieldSet)} will fail of the FieldSet contains fields that
	 * cannot be mapped to the bean.
	 * @param strict indicator
	 */
	public void setStrict(boolean strict) {
		this.strict = strict;
	}

	/**
	 * Public setter for the 'conversionService' property. {@link #createBinder(Object)}
	 * will use it if not null.
	 * @param conversionService {@link ConversionService} to be used for type conversions
	 */
	public void setConversionService(ConversionService conversionService) {
		this.conversionService = conversionService;
	}

	/**
	 * Specify the {@link PropertyEditor custom editors} to register.
	 * @param customEditors a map of Class to PropertyEditor (or class name to
	 * PropertyEditor).
	 * @see CustomEditorConfigurer#setCustomEditors(Map)
	 */
	@Override
	public void setCustomEditors(Map<? extends Object, ? extends PropertyEditor> customEditors) {
		this.isCustomEditorsSet = true;
		super.setCustomEditors(customEditors);
	}

	private static class DistanceHolder {

		private final Class<?> cls;

		private final int distance;

		public DistanceHolder(Class<?> cls, int distance) {
			this.cls = cls;
			this.distance = distance;

		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((cls == null) ? 0 : cls.hashCode());
			result = prime * result + distance;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			DistanceHolder other = (DistanceHolder) obj;
			if (cls == null) {
				if (other.cls != null)
					return false;
			}
			else if (!cls.equals(other.cls))
				return false;
			if (distance != other.distance)
				return false;
			return true;
		}

	}

}

相关信息

spring-batch 源码目录

相关文章

spring-batch ArrayFieldSetMapper 源码

spring-batch DefaultLineMapper 源码

spring-batch FieldSetMapper 源码

spring-batch JsonLineMapper 源码

spring-batch PassThroughFieldSetMapper 源码

spring-batch PassThroughLineMapper 源码

spring-batch PatternMatchingCompositeLineMapper 源码

spring-batch PropertyMatches 源码

spring-batch RecordFieldSetMapper 源码

spring-batch package-info 源码

0  赞