Replace BeanPropertyRowMapper with BeanWrapperRowMapper.
Use the BeanWrapper to bind values to beans. Based on the BeanWrapperFieldSetMapper.
This commit is contained in:
committed by
Michael Minella
parent
371474fb48
commit
534c5b31df
@@ -1,335 +0,0 @@
|
||||
/*
|
||||
* Copyright 2006-2014 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.batch.item.excel.mapping;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.batch.item.excel.RowMapper;
|
||||
import org.springframework.batch.item.excel.support.rowset.RowSet;
|
||||
import org.springframework.batch.item.excel.support.rowset.RowSetMetaData;
|
||||
import org.springframework.beans.*;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* {@link RowMapper} implementation that converts a row into a new instance
|
||||
* of the specified mapped target class. The mapped target class must be a
|
||||
* top-level class and it must have a default or no-arg constructor.
|
||||
*
|
||||
* Column values are mapped based on matching the column name as obtained from row set
|
||||
* metadata to public setters for the corresponding properties. The names are matched either
|
||||
* directly or by transforming a name separating the parts with underscores to the same name
|
||||
* using "camel" case.
|
||||
*
|
||||
* Mapping is provided for fields in the target class for many common types, e.g.:
|
||||
* String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long,
|
||||
* float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
|
||||
*
|
||||
* For 'null' values read from the Excel document, we will attempt to call the setter, but in the case of
|
||||
* Java primitives, this causes a TypeMismatchException. This class can be configured (using the
|
||||
* primitivesDefaultedForNullValue property) to trap this exception and use the primitives default value.
|
||||
* Be aware that if you use the values from the generated bean to update the database the primitive value
|
||||
* will have been set to the primitive's default value instead of null.
|
||||
*
|
||||
* Please note that this class is designed to provide convenience rather than high performance.
|
||||
* For best performance, consider using a custom {@link RowMapper} implementation.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @param <T> The type
|
||||
* @since 0.5.0
|
||||
*/
|
||||
public class BeanPropertyRowMapper<T> implements RowMapper<T>, BeanFactoryAware, InitializingBean {
|
||||
|
||||
/**
|
||||
* Logger available to subclasses
|
||||
*/
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* The class we are mapping to
|
||||
*/
|
||||
private Class<T> type;
|
||||
|
||||
/**
|
||||
* The name of the bean we are mapping to
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Whether we're strictly validating
|
||||
*/
|
||||
private boolean checkFullyPopulated = false;
|
||||
|
||||
/**
|
||||
* Whether we're defaulting primitives when mapping a null value
|
||||
*/
|
||||
private boolean primitivesDefaultedForNullValue = false;
|
||||
|
||||
/**
|
||||
* Map of the fields we provide mapping for
|
||||
*/
|
||||
private Map<String, PropertyDescriptor> mappedFields;
|
||||
|
||||
/**
|
||||
* Set of bean properties we provide mapping for
|
||||
*/
|
||||
private Set<String> mappedProperties;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
/**
|
||||
* Create a new BeanPropertyRowMapper for bean-style configuration.
|
||||
*
|
||||
* @see #setTargetType
|
||||
* @see #setPrototypeBeanName
|
||||
* @see #setCheckFullyPopulated
|
||||
*/
|
||||
public BeanPropertyRowMapper() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The bean name (id) for an object that can be populated from the field set
|
||||
* that will be passed into {@link #mapRow(RowSet)}. 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 #mapRow(RowSet)}.<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<T> type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the mapping metadata for the given class.
|
||||
*
|
||||
* @param mappedClass the mapped class.
|
||||
*/
|
||||
protected void initialize(Class<T> mappedClass) {
|
||||
this.mappedFields = new HashMap<String, PropertyDescriptor>();
|
||||
this.mappedProperties = new HashSet<String>();
|
||||
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);
|
||||
for (PropertyDescriptor pd : pds) {
|
||||
if (pd.getWriteMethod() != null) {
|
||||
this.mappedFields.put(pd.getName().toLowerCase(), pd);
|
||||
String underscoredName = underscoreName(pd.getName());
|
||||
if (!pd.getName().toLowerCase().equals(underscoredName)) {
|
||||
this.mappedFields.put(underscoredName, pd);
|
||||
}
|
||||
this.mappedProperties.add(pd.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a name in camelCase to an underscored name in lower case.
|
||||
* Any upper case letters are converted to lower case with a preceding underscore.
|
||||
*
|
||||
* @param name the string containing original name
|
||||
* @return the converted name
|
||||
*/
|
||||
private String underscoreName(String name) {
|
||||
if (!StringUtils.hasLength(name)) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(name.substring(0, 1).toLowerCase());
|
||||
for (int i = 1; i < name.length(); i++) {
|
||||
String s = name.substring(i, i + 1);
|
||||
String slc = s.toLowerCase();
|
||||
if (!s.equals(slc)) {
|
||||
result.append("_").append(slc);
|
||||
} else {
|
||||
result.append(s);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we're strictly validating that all bean properties have been
|
||||
* mapped from corresponding database fields. Default is {@code false},
|
||||
* accepting unpopulated properties in the target bean.
|
||||
*
|
||||
* @param checkFullyPopulated true or false
|
||||
*/
|
||||
public void setCheckFullyPopulated(boolean checkFullyPopulated) {
|
||||
this.checkFullyPopulated = checkFullyPopulated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether we're strictly validating that all bean properties have been
|
||||
* mapped from corresponding database fields.
|
||||
*
|
||||
* @return true when resulting bean should be checked
|
||||
*/
|
||||
public boolean isCheckFullyPopulated() {
|
||||
return this.checkFullyPopulated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we're defaulting Java primitives in the case of mapping a null value
|
||||
* from corresponding database fields.
|
||||
*
|
||||
* Default is {@code false}, throwing an exception when nulls are mapped to Java primitives.
|
||||
*
|
||||
* @param primitivesDefaultedForNullValue should default values be used when read value is {@code null}
|
||||
*/
|
||||
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
|
||||
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether we're defaulting Java primitives in the case of mapping a null value
|
||||
* from corresponding database fields.
|
||||
*
|
||||
* @return {@code true} when default values be used when read value is {@code null}
|
||||
*/
|
||||
public boolean isPrimitivesDefaultedForNullValue() {
|
||||
return primitivesDefaultedForNullValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the values for all columns in the current row.
|
||||
*
|
||||
* Utilizes public setters and result set metadata.
|
||||
*
|
||||
* @see java.sql.ResultSetMetaData
|
||||
*/
|
||||
@Override
|
||||
public T mapRow(RowSet rs) throws Exception {
|
||||
T mappedObject = getBean();
|
||||
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
|
||||
initBeanWrapper(bw);
|
||||
|
||||
RowSetMetaData rsmd = rs.getMetaData();
|
||||
int columnCount = rsmd.getColumnCount();
|
||||
Set<String> populatedProperties = (isCheckFullyPopulated() ? new HashSet<String>() : null);
|
||||
|
||||
for (int index = 0; index < columnCount; index++) {
|
||||
String column = rsmd.getColumnName(index);
|
||||
PropertyDescriptor pd = this.mappedFields.get(column.replaceAll(" ", "").toLowerCase());
|
||||
if (pd != null) {
|
||||
String value = rs.getColumnValue(index);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Mapping column '" + column + "' to property '" +
|
||||
pd.getName() + "' of type " + pd.getPropertyType());
|
||||
}
|
||||
try {
|
||||
|
||||
bw.setPropertyValue(pd.getName(), value);
|
||||
} catch (TypeMismatchException e) {
|
||||
if (value == null && primitivesDefaultedForNullValue) {
|
||||
logger.debug("Intercepted TypeMismatchException for row " + rs.getCurrentRowIndex() +
|
||||
" on sheet " + rsmd.getSheetName() + " and column '" + column + "' with value " + value +
|
||||
" when setting property '" + pd.getName() + "' of type " + pd.getPropertyType() +
|
||||
" on object: " + mappedObject);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (populatedProperties != null) {
|
||||
populatedProperties.add(pd.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) {
|
||||
throw new IllegalStateException("Given RowSet does not contain all fields " +
|
||||
"necessary to populate object of class [" + mappedObject.getClass() + "]: " + this.mappedProperties);
|
||||
}
|
||||
|
||||
return mappedObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the given BeanWrapper to be used for row mapping.
|
||||
* To be called for each row.
|
||||
* <p>The default implementation is empty. Can be overridden in subclasses.
|
||||
*
|
||||
* @param bw the BeanWrapper to initialize
|
||||
*/
|
||||
protected void initBeanWrapper(BeanWrapper bw) {
|
||||
}
|
||||
|
||||
private T getBean() {
|
||||
if (name != null) {
|
||||
return (T) beanFactory.getBean(name);
|
||||
}
|
||||
try {
|
||||
return type.newInstance();
|
||||
} catch (InstantiationException e) {
|
||||
ReflectionUtils.handleReflectionException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
ReflectionUtils.handleReflectionException(e);
|
||||
}
|
||||
// should not happen
|
||||
throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@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.");
|
||||
initialize((Class<T>) getBean().getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create a new BeanPropertyRowMapper
|
||||
* (with the mapped class specified only once).
|
||||
*
|
||||
* @param targetType the class that each row should be mapped to
|
||||
* @param <T> the targetType
|
||||
* @return the newly created instance
|
||||
*/
|
||||
public static <T> BeanPropertyRowMapper<T> newInstance(Class<T> targetType) {
|
||||
BeanPropertyRowMapper<T> newInstance = new BeanPropertyRowMapper<T>();
|
||||
newInstance.setTargetType(targetType);
|
||||
return newInstance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package org.springframework.batch.item.excel.mapping;
|
||||
|
||||
import org.springframework.batch.item.excel.RowMapper;
|
||||
import org.springframework.batch.item.excel.support.rowset.RowSet;
|
||||
import org.springframework.batch.support.DefaultPropertyEditorRegistrar;
|
||||
import org.springframework.beans.*;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.DataBinder;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* {@link RowMapper} implementation based on bean property paths. The
|
||||
* {@link RowSet} 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 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 RowSet}names. They will be converted to
|
||||
* nested bean properties inside the prototype. The {@link RowSet} 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 RowSet} values are converted to the
|
||||
* desired type for injecting into the prototype there are several choices. You
|
||||
* can inject {@link java.beans.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 RowSet} implementation.<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 Marten Deinum
|
||||
* @since 0.5.0
|
||||
*/
|
||||
public class BeanWrapperRowMapper<T> extends DefaultPropertyEditorRegistrar implements RowMapper<T>, BeanFactoryAware, InitializingBean {
|
||||
|
||||
private String name;
|
||||
|
||||
private Class<? extends T> type;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
private ConcurrentMap<DistanceHolder, ConcurrentMap<String, String>> propertiesMatched = new ConcurrentHashMap<DistanceHolder, ConcurrentMap<String, String>>();
|
||||
|
||||
private int distanceLimit = 5;
|
||||
|
||||
private boolean strict = true;
|
||||
|
||||
/*
|
||||
* (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 #mapRow(org.springframework.batch.item.excel.support.rowset.RowSet)}. 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 #mapRow(org.springframework.batch.item.excel.support.rowset.RowSet)}.<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.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the {@link org.springframework.batch.item.file.transform.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 org.springframework.validation.BindException if there is a type conversion or other error (if
|
||||
* the {@link org.springframework.validation.DataBinder} from {@link #createBinder(Object)} has errors
|
||||
* after binding).
|
||||
* @throws org.springframework.beans.NotWritablePropertyException if the {@link org.springframework.batch.item.file.transform.FieldSet} contains a
|
||||
* field that cannot be mapped to a bean property.
|
||||
* @see org.springframework.batch.item.file.mapping.FieldSetMapper#mapFieldSet(org.springframework.batch.item.file.transform.FieldSet)
|
||||
*/
|
||||
@Override
|
||||
public T mapRow(RowSet rs) throws BindException {
|
||||
T copy = getBean();
|
||||
DataBinder binder = createBinder(copy);
|
||||
binder.bind(new MutablePropertyValues(getBeanProperties(copy, rs.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(org.springframework.beans.PropertyEditorRegistry)}.
|
||||
*
|
||||
* @param target the 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);
|
||||
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(org.springframework.beans.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.newInstance();
|
||||
} catch (InstantiationException e) {
|
||||
ReflectionUtils.handleReflectionException(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
ReflectionUtils.handleReflectionException(e);
|
||||
}
|
||||
// should not happen
|
||||
throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bean
|
||||
* @param properties
|
||||
* @return
|
||||
*/
|
||||
private Properties getBeanProperties(Object bean, Properties 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<String, String>());
|
||||
}
|
||||
Map<String, String> matches = new HashMap<String, String>(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<String, String>(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).newInstance();
|
||||
wrapper.setPropertyValue(nestedName, nestedValue);
|
||||
} catch (InstantiationException e) {
|
||||
ReflectionUtils.handleReflectionException(e);
|
||||
} catch (IllegalAccessException 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 #mapRow(RowSet)} will fail if the RowSet contains fields
|
||||
* that cannot be mapped to the bean.
|
||||
*
|
||||
* @param strict fail if non-mappable properties are found
|
||||
*/
|
||||
public void setStrict(boolean strict) {
|
||||
this.strict = strict;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package org.springframework.batch.item.excel.mapping;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class for calculating bean property matches, according to.
|
||||
* Used by BeanWrapperImpl to suggest alternatives for an invalid property name.<br>
|
||||
*
|
||||
* Copied and slightly modified from Spring core,
|
||||
*
|
||||
* @author Alef Arendsen
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @author Dave Syer
|
||||
*
|
||||
* @since 1.0
|
||||
* @see #forProperty(String, Class)
|
||||
*/
|
||||
final class PropertyMatches {
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Static section
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/** Default maximum property distance: 2 */
|
||||
public static final int DEFAULT_MAX_DISTANCE = 2;
|
||||
|
||||
|
||||
/**
|
||||
* Create PropertyMatches for the given bean property.
|
||||
* @param propertyName the name of the property to find possible matches for
|
||||
* @param beanClass the bean class to search for matches
|
||||
*/
|
||||
public static PropertyMatches forProperty(String propertyName, Class<?> beanClass) {
|
||||
return forProperty(propertyName, beanClass, DEFAULT_MAX_DISTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create PropertyMatches for the given bean property.
|
||||
* @param propertyName the name of the property to find possible matches for
|
||||
* @param beanClass the bean class to search for matches
|
||||
* @param maxDistance the maximum property distance allowed for matches
|
||||
*/
|
||||
public static PropertyMatches forProperty(String propertyName, Class<?> beanClass, int maxDistance) {
|
||||
return new PropertyMatches(propertyName, beanClass, maxDistance);
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Instance section
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
private final String propertyName;
|
||||
|
||||
private String[] possibleMatches;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new PropertyMatches instance for the given property.
|
||||
*/
|
||||
private PropertyMatches(String propertyName, Class<?> beanClass, int maxDistance) {
|
||||
this.propertyName = propertyName;
|
||||
this.possibleMatches = calculateMatches(BeanUtils.getPropertyDescriptors(beanClass), maxDistance);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the calculated possible matches.
|
||||
*/
|
||||
public String[] getPossibleMatches() {
|
||||
return possibleMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an error message for the given invalid property name,
|
||||
* indicating the possible property matches.
|
||||
*/
|
||||
public String buildErrorMessage() {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
buf.append("Bean property '");
|
||||
buf.append(this.propertyName);
|
||||
buf.append("' is not writable or has an invalid setter method. ");
|
||||
|
||||
if (ObjectUtils.isEmpty(this.possibleMatches)) {
|
||||
buf.append("Does the parameter type of the setter match the return type of the getter?");
|
||||
}
|
||||
else {
|
||||
buf.append("Did you mean ");
|
||||
for (int i = 0; i < this.possibleMatches.length; i++) {
|
||||
buf.append('\'');
|
||||
buf.append(this.possibleMatches[i]);
|
||||
if (i < this.possibleMatches.length - 2) {
|
||||
buf.append("', ");
|
||||
}
|
||||
else if (i == this.possibleMatches.length - 2){
|
||||
buf.append("', or ");
|
||||
}
|
||||
}
|
||||
buf.append("'?");
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate possible property alternatives for the given property and
|
||||
* class. Internally uses the <code>getStringDistance</code> method, which
|
||||
* in turn uses the Levenshtein algorithm to determine the distance between
|
||||
* two Strings.
|
||||
* @param propertyDescriptors the JavaBeans property descriptors to search
|
||||
* @param maxDistance the maximum distance to accept
|
||||
*/
|
||||
private String[] calculateMatches(PropertyDescriptor[] propertyDescriptors, int maxDistance) {
|
||||
List<String> candidates = new ArrayList<String>();
|
||||
for (int i = 0; i < propertyDescriptors.length; i++) {
|
||||
if (propertyDescriptors[i].getWriteMethod() != null) {
|
||||
String possibleAlternative = propertyDescriptors[i].getName();
|
||||
int distance = calculateStringDistance(this.propertyName, possibleAlternative);
|
||||
if (distance <= maxDistance) {
|
||||
candidates.add(possibleAlternative);
|
||||
}
|
||||
}
|
||||
}
|
||||
Collections.sort(candidates);
|
||||
return StringUtils.toStringArray(candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance between the given two Strings
|
||||
* according to the Levenshtein algorithm.
|
||||
* @param s1 the first String
|
||||
* @param s2 the second String
|
||||
* @return the distance value
|
||||
*/
|
||||
private int calculateStringDistance(String s1, String s2) {
|
||||
if (s1.length() == 0) {
|
||||
return s2.length();
|
||||
}
|
||||
if (s2.length() == 0) {
|
||||
return s1.length();
|
||||
}
|
||||
int d[][] = new int[s1.length() + 1][s2.length() + 1];
|
||||
|
||||
for (int i = 0; i <= s1.length(); i++) {
|
||||
d[i][0] = i;
|
||||
}
|
||||
for (int j = 0; j <= s2.length(); j++) {
|
||||
d[0][j] = j;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= s1.length(); i++) {
|
||||
char s_i = s1.charAt(i - 1);
|
||||
for (int j = 1; j <= s2.length(); j++) {
|
||||
int cost;
|
||||
char t_j = s2.charAt(j - 1);
|
||||
if (Character.toLowerCase(s_i) == Character.toLowerCase(t_j)) {
|
||||
cost = 0;
|
||||
} else {
|
||||
cost = 1;
|
||||
}
|
||||
d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1),
|
||||
d[i - 1][j - 1] + cost);
|
||||
}
|
||||
}
|
||||
|
||||
return d[s1.length()][s2.length()];
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.batch.item.ExecutionContext;
|
||||
import org.springframework.batch.item.Player;
|
||||
import org.springframework.batch.item.excel.mapping.BeanPropertyRowMapper;
|
||||
import org.springframework.batch.item.excel.mapping.BeanWrapperRowMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -32,7 +32,7 @@ public class BeanPropertyItemReaderTest {
|
||||
|
||||
reader = new MockExcelItemReader<Player>(sheet);
|
||||
|
||||
BeanPropertyRowMapper<Player> rowMapper = new BeanPropertyRowMapper<Player>();
|
||||
BeanWrapperRowMapper<Player> rowMapper = new BeanWrapperRowMapper<Player>();
|
||||
rowMapper.setTargetType(Player.class);
|
||||
rowMapper.afterPropertiesSet();
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.batch.item.ExecutionContext;
|
||||
import org.springframework.batch.item.Player;
|
||||
import org.springframework.batch.item.excel.MockExcelItemReader;
|
||||
import org.springframework.batch.item.excel.MockSheet;
|
||||
import org.springframework.batch.item.excel.mapping.BeanPropertyRowMapper;
|
||||
import org.springframework.batch.item.excel.support.rowset.DefaultRowSet;
|
||||
import org.springframework.batch.item.excel.mapping.BeanWrapperRowMapper;
|
||||
import org.springframework.batch.item.excel.support.rowset.DefaultRowSetFactory;
|
||||
import org.springframework.batch.item.excel.support.rowset.StaticColumnNameExtractor;
|
||||
|
||||
@@ -36,7 +33,7 @@ public class BeanPropertyWithStaticHeaderItemReaderTest {
|
||||
|
||||
reader = new MockExcelItemReader<Player>(sheet);
|
||||
|
||||
BeanPropertyRowMapper<Player> rowMapper = new BeanPropertyRowMapper<Player>();
|
||||
BeanWrapperRowMapper<Player> rowMapper = new BeanWrapperRowMapper<Player>();
|
||||
rowMapper.setTargetType(Player.class);
|
||||
rowMapper.afterPropertiesSet();
|
||||
|
||||
|
||||
@@ -17,19 +17,20 @@ import java.util.List;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Created by in329dei on 17-9-2014.
|
||||
* @author Marten Deinum
|
||||
* @since 0.5.0
|
||||
*/
|
||||
public class BeanPropertyRowMapperTest {
|
||||
public class BeanWrapperRowMapperTest {
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void givenNoNameWhenInitCompleteThenIllegalStateShouldOccur() throws Exception {
|
||||
BeanPropertyRowMapper mapper = new BeanPropertyRowMapper();
|
||||
BeanWrapperRowMapper mapper = new BeanWrapperRowMapper();
|
||||
mapper.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAValidRowWhenMappingThenAValidPlayerShouldBeConstructed() throws Exception {
|
||||
BeanPropertyRowMapper<Player> mapper = new BeanPropertyRowMapper<Player>();
|
||||
BeanWrapperRowMapper<Player> mapper = new BeanWrapperRowMapper<Player>();
|
||||
mapper.setTargetType(Player.class);
|
||||
mapper.afterPropertiesSet();
|
||||
|
||||
@@ -59,7 +60,7 @@ public class BeanPropertyRowMapperTest {
|
||||
public void givenAValidRowWhenMappingThenAValidPlayerShouldBeConstructedBasedOnPrototype() throws Exception {
|
||||
|
||||
ApplicationContext ctx = new AnnotationConfigApplicationContext(TestConfig.class);
|
||||
BeanPropertyRowMapper<Player> mapper = new BeanPropertyRowMapper<Player>();
|
||||
BeanWrapperRowMapper<Player> mapper = new BeanWrapperRowMapper<Player>();
|
||||
mapper.setPrototypeBeanName("player");
|
||||
mapper.setBeanFactory(ctx);
|
||||
mapper.afterPropertiesSet();
|
||||
Reference in New Issue
Block a user