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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -216,6 +216,11 @@ public class FieldSetTests extends TestCase {
|
||||
assertEquals(543, fieldSet.readLong("Long"));
|
||||
}
|
||||
|
||||
public void testReadLongWithPadding() throws Exception {
|
||||
fieldSet = new FieldSet(new String[] {"000009"});
|
||||
assertEquals(9, fieldSet.readLong(0));
|
||||
}
|
||||
|
||||
public void testReadIntWithNullValue() {
|
||||
assertEquals(5, fieldSet.readInt(10, 5));
|
||||
assertEquals(5, fieldSet.readInt("Null", 5));
|
||||
|
||||
@@ -16,17 +16,19 @@
|
||||
|
||||
package org.springframework.batch.io.file.support.mapping;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.springframework.batch.io.file.FieldSet;
|
||||
import org.springframework.batch.io.file.support.mapping.BeanWrapperFieldSetMapper;
|
||||
import org.springframework.batch.support.IntArrayPropertyEditor;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
import org.springframework.beans.NotWritablePropertyException;
|
||||
import org.springframework.beans.propertyeditors.CustomNumberEditor;
|
||||
import org.springframework.beans.propertyeditors.PropertiesEditor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
@@ -40,16 +42,18 @@ public class BeanWrapperFieldSetMapperTests extends TestCase {
|
||||
mapper.setPrototypeBeanName("foo");
|
||||
try {
|
||||
mapper.afterPropertiesSet();
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void testNameNorTypeSpecified() throws Exception {
|
||||
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
|
||||
try {
|
||||
mapper.afterPropertiesSet();
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
@@ -161,8 +165,7 @@ public class BeanWrapperFieldSetMapperTests extends TestCase {
|
||||
context.getBeanFactory().registerSingleton("bean", testNestedC);
|
||||
mapper.setPrototypeBeanName("bean");
|
||||
|
||||
FieldSet fieldSet = new FieldSet(new String[] { "1" }, new String[] {
|
||||
"foo" });
|
||||
FieldSet fieldSet = new FieldSet(new String[] { "1" }, new String[] { "foo" });
|
||||
|
||||
TestNestedC result = (TestNestedC) mapper.mapLine(fieldSet);
|
||||
|
||||
@@ -258,31 +261,94 @@ public class BeanWrapperFieldSetMapperTests extends TestCase {
|
||||
wrapper.setPropertyValues(props);
|
||||
assertEquals(4, result.numbers[3]);
|
||||
}
|
||||
|
||||
//BeanWrapperFieldSetMapper doesn't currently support nesting with collections.
|
||||
public void testNestedList(){
|
||||
|
||||
|
||||
// BeanWrapperFieldSetMapper doesn't currently support nesting with
|
||||
// collections.
|
||||
public void testNestedList() {
|
||||
|
||||
TestNestedList nestedList = new TestNestedList();
|
||||
List nestedC = new ArrayList();
|
||||
nestedC.add(new TestNestedC());
|
||||
nestedC.add(new TestNestedC());
|
||||
nestedC.add(new TestNestedC());
|
||||
nestedList.setNestedC(nestedC);
|
||||
|
||||
|
||||
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
mapper.setBeanFactory(context);
|
||||
context.getBeanFactory().registerSingleton("bean", nestedList);
|
||||
mapper.setPrototypeBeanName("bean");
|
||||
|
||||
FieldSet fieldSet = new FieldSet(new String[]{ "1", "2", "3"}, new String[]{"NestedC[0].Value", "NestedC[1].Value", "NestedC[2].Value"});
|
||||
|
||||
mapper.mapLine(fieldSet);
|
||||
|
||||
assertEquals(((TestNestedC) nestedList.getNestedC().get(0)).getValue(), 1);
|
||||
assertEquals(((TestNestedC) nestedList.getNestedC().get(1)).getValue(), 2);
|
||||
assertEquals(((TestNestedC) nestedList.getNestedC().get(2)).getValue(), 3);
|
||||
|
||||
FieldSet fieldSet = new FieldSet(new String[] { "1", "2", "3" }, new String[] { "NestedC[0].Value",
|
||||
"NestedC[1].Value", "NestedC[2].Value" });
|
||||
|
||||
mapper.mapLine(fieldSet);
|
||||
|
||||
assertEquals(1, ((TestNestedC) nestedList.getNestedC().get(0)).getValue());
|
||||
assertEquals(2, ((TestNestedC) nestedList.getNestedC().get(1)).getValue());
|
||||
assertEquals(3, ((TestNestedC) nestedList.getNestedC().get(2)).getValue());
|
||||
|
||||
}
|
||||
|
||||
public void testPaddedLongWithNoEditor() throws Exception {
|
||||
|
||||
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
|
||||
mapper.setTargetType(TestObject.class);
|
||||
|
||||
FieldSet fieldSet = new FieldSet(new String[] { "00009" }, new String[] { "varLong" });
|
||||
|
||||
try {
|
||||
mapper.mapLine(fieldSet);
|
||||
fail("Expected BindingException");
|
||||
}
|
||||
catch (BindingException e) {
|
||||
assertTrue("Message does not contain source value: " + e.getMessage(), e.getMessage().indexOf("0009") >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void testSetCustomEditorsWithInvalidTypeName() throws Exception {
|
||||
|
||||
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
|
||||
try {
|
||||
mapper.setCustomEditors(Collections.singletonMap("FOO", new CustomNumberEditor(Long.class, true)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
public void testSetCustomEditorsWithInvalidType() throws Exception {
|
||||
|
||||
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
|
||||
try {
|
||||
mapper.setCustomEditors(Collections.singletonMap(new Object(), new CustomNumberEditor(Long.class, true)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void testSetCustomEditorsWithInvalidEditor() throws Exception {
|
||||
|
||||
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
|
||||
try {
|
||||
mapper.setCustomEditors(Collections.singletonMap(Long.class, "FOO"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
public void testPaddedLongWithEditor() throws Exception {
|
||||
|
||||
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
|
||||
mapper.setTargetType(TestObject.class);
|
||||
|
||||
FieldSet fieldSet = new FieldSet(new String[] { "00009" }, new String[] { "varLong" });
|
||||
|
||||
mapper.setCustomEditors(Collections.singletonMap(Long.TYPE, new CustomNumberEditor(Long.class, NumberFormat
|
||||
.getNumberInstance(), true)));
|
||||
TestObject bean = (TestObject) mapper.mapLine(fieldSet);
|
||||
|
||||
assertEquals(9, bean.getVarLong());
|
||||
}
|
||||
|
||||
private static class BeanWithIntArray {
|
||||
@@ -292,7 +358,7 @@ public class BeanWrapperFieldSetMapperTests extends TestCase {
|
||||
this.numbers = numbers;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestNestedList {
|
||||
|
||||
List nestedC;
|
||||
@@ -304,8 +370,6 @@ public class BeanWrapperFieldSetMapperTests extends TestCase {
|
||||
public void setNestedC(List nestedC) {
|
||||
this.nestedC = nestedC;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -341,7 +405,7 @@ public class BeanWrapperFieldSetMapperTests extends TestCase {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class TestNestedB {
|
||||
private String valueA;
|
||||
|
||||
@@ -364,7 +428,7 @@ public class BeanWrapperFieldSetMapperTests extends TestCase {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class TestNestedC {
|
||||
private int value;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user