Replace BeanPropertyRowMapper with BeanWrapperRowMapper.

Use the BeanWrapper to bind values to beans. Based on the BeanWrapperFieldSetMapper.
This commit is contained in:
Marten Deinum
2015-02-03 22:23:47 +01:00
committed by Michael Minella
parent 371474fb48
commit 534c5b31df
6 changed files with 581 additions and 347 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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()];
}
}

View File

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

View File

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

View File

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