RESOLVED - issue BATCH-154: Re-implement BeanWrapperFieldSetMapper using a DataBinder

http://opensource.atlassian.com/projects/spring/browse/BATCH-154

Added new config options to BeanWrapperFieldSetMapper for custom property editors and extension points to create a custom binder if necessary.
This commit is contained in:
dsyer
2007-12-19 21:13:43 +00:00
parent 112150566c
commit d0c42b7be1
4 changed files with 263 additions and 61 deletions

View File

@@ -16,25 +16,33 @@
package org.springframework.batch.io.file.support.mapping;
import java.beans.PropertyEditor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.batch.io.file.FieldSet;
import org.springframework.batch.io.file.FieldSetMapper;
import org.springframework.beans.BeanWrapper;
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.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.DataBinder;
import org.springframework.validation.ObjectError;
/**
* {@link FieldSetMapper} implementation based on bean property paths. The
@@ -72,8 +80,7 @@ import org.springframework.util.ReflectionUtils;
* @author Dave Syer
*
*/
public class BeanWrapperFieldSetMapper implements FieldSetMapper,
BeanFactoryAware, InitializingBean {
public class BeanWrapperFieldSetMapper implements FieldSetMapper, BeanFactoryAware, InitializingBean {
private String name;
@@ -81,6 +88,8 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
private BeanFactory beanFactory;
private Map customEditors;
private static Map propertiesMatched = new HashMap();
private static int distanceLimit = 5;
@@ -103,8 +112,7 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
* 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
* @param name the name of a prototype bean in the enclosing BeanFactory
*/
public void setPrototypeBeanName(String name) {
this.name = name;
@@ -118,8 +126,7 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
* Either this property or the prototype bean name must be specified, but
* not both.
*
* @param type
* the type to set
* @param type the type to set
*/
public void setTargetType(Class type) {
this.type = type;
@@ -128,50 +135,111 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
/**
* Check that precisely one of type or prototype bean name is specified.
*
* @throws IllegalStateException
* if neither is set or both properties are set.
* @throws IllegalStateException if neither is set or both properties are
* set.
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
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(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 FieldSet} to an object retrieved from the enclosing Spring
* context.
* context, or to a new instance of the required type if no prototype is
* available.
*
* @throws NotWritablePropertyException
* if the {@link FieldSet} contains a field that cannot be
* mapped to a bean property.
* @throws NotWritablePropertyException if the {@link FieldSet} contains a
* field that cannot be mapped to a bean property.
* @throws BindingException if there is a type conversion or other error (if
* the {@link DataBinder} from {@link #createBinder(Object)} has errors
* after binding).
*
* @see org.springframework.batch.io.file.FieldSetMapper#mapLine(org.springframework.batch.io.file.FieldSet)
*/
public Object mapLine(FieldSet fs) {
Object copy = getBean();
BeanWrapper wrapper = new BeanWrapperImpl(copy);
wrapper.setPropertyValues(getBeanProperties(copy, fs.getProperties()));
DataBinder binder = createBinder(copy);
binder.bind(new MutablePropertyValues(getBeanProperties(copy, fs.getProperties())));
if (binder.getBindingResult().hasErrors()) {
List errors = binder.getBindingResult().getAllErrors();
List messages = new ArrayList(errors.size());
for (Iterator iterator = errors.iterator(); iterator.hasNext();) {
ObjectError error = (ObjectError) iterator.next();
messages.add(error.getDefaultMessage());
}
throw new BindingException("" + messages);
}
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 #registerPropertyEditors(DataBinder)}.
*
* @param target
* @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(false);
initBinder(binder);
registerPropertyEditors(binder);
return binder;
}
/**
* Register property editors with the DataBinder. If any property editors
* have been supplied they are registered for use when binding a field set.
*
* @param binder new binder instance
*/
protected void registerPropertyEditors(DataBinder binder) {
if (this.customEditors != null) {
for (Iterator it = customEditors.entrySet().iterator(); it.hasNext();) {
Map.Entry entry = (Map.Entry) it.next();
Class key = (Class) entry.getKey();
PropertyEditor value = (PropertyEditor) entry.getValue();
binder.registerCustomEditor(key, value);
}
}
}
/**
* 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 should be done in
* {@link #registerPropertyEditors(PropertyEditorRegistry)}, not here! This
* method will only be called when a <b>new</b> data binder is created.
* @param binder new binder instance
* @see #createBinder(RequestContext, Object)
*/
protected void initBinder(DataBinder binder) {
}
private Object getBean() {
if (name != null) {
return beanFactory.getBean(name);
}
try {
return type.newInstance();
} catch (InstantiationException e) {
ReflectionUtils.handleReflectionException(e);
} catch (IllegalAccessException e) {
}
catch (InstantiationException e) {
ReflectionUtils.handleReflectionException(e);
}
throw new IllegalStateException(
"Internal error: could not create bean instance for mapping."); // should
// not
// happen
catch (IllegalAccessException e) {
ReflectionUtils.handleReflectionException(e);
}
// should not happen
throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
}
/**
@@ -214,8 +282,7 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
Class cls = bean.getClass();
int index = PropertyAccessorUtils
.getFirstNestedPropertySeparatorIndex(key);
int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(key);
String prefix;
String suffix;
@@ -229,9 +296,9 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
return null;
}
Object nestedValue = new BeanWrapperImpl(bean)
.getPropertyValue(nestedName);
return nestedName + "." + findPropertyName(nestedValue, suffix);
Object nestedValue = new BeanWrapperImpl(bean).getPropertyValue(nestedName);
String nestedPropertyName = findPropertyName(nestedValue, suffix);
return nestedPropertyName == null ? null : nestedName + "." + nestedPropertyName;
}
String name = null;
@@ -241,21 +308,22 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
if (index > 0) {
prefix = key.substring(0, index);
suffix = key.substring(index);
} else {
}
else {
prefix = key;
suffix = "";
}
while (name == null && distance <= distanceLimit) {
String[] candidates = PropertyMatches.forProperty(prefix, cls,
distance).getPossibleMatches();
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 {
}
else {
name = candidate + suffix;
}
}
@@ -264,10 +332,44 @@ public class BeanWrapperFieldSetMapper implements FieldSetMapper,
return name;
}
private void switchPropertyNames(Properties properties, String oldName,
String newName) {
private void switchPropertyNames(Properties properties, String oldName, String newName) {
String value = properties.getProperty(oldName);
properties.remove(oldName);
properties.setProperty(newName, value);
}
/**
* Specify the {@link PropertyEditor custom editors} to apply to target
* beans mapped with this {@link FieldSetMapper}.
*
*
* @param customEditors a map of Class to PropertyEditor (or class name to
* PropertyEditor).
* @see CustomEditorConfigurer#setCustomEditors(Map)
*/
public void setCustomEditors(Map customEditors) {
this.customEditors = new HashMap();
for (Iterator it = customEditors.entrySet().iterator(); it.hasNext();) {
Map.Entry entry = (Map.Entry) it.next();
Object key = entry.getKey();
Class requiredType = null;
if (key instanceof Class) {
requiredType = (Class) key;
}
else if (key instanceof String) {
String className = (String) key;
requiredType = ClassUtils.resolveClassName(className, getClass().getClassLoader());
}
else {
throw new IllegalArgumentException("Invalid key [" + key
+ "] for custom editor: needs to be Class or String.");
}
Object value = entry.getValue();
if (!(value instanceof PropertyEditor)) {
throw new IllegalArgumentException("Mapped value [" + value + "] for custom editor key [" + key
+ "] is not of required type [" + PropertyEditor.class.getName() + "]");
}
this.customEditors.put(requiredType, value);
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2006-2007 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.io.file.support.mapping;
/**
* @author Dave Syer
*
*/
public class BindingException extends RuntimeException {
/**
* @param msg
*/
public BindingException(String msg) {
super(msg);
}
}