diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapper.java index 9a2d1d70d..a92ff6f09 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapper.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapper.java @@ -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)}. + *
+ * Note that registration of custom property editors should be done in + * {@link #registerPropertyEditors(PropertyEditorRegistry)}, not here! This + * method will only be called when a new 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); + } + } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BindingException.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BindingException.java new file mode 100644 index 000000000..a0cc37b8b --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BindingException.java @@ -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); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/FieldSetTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/FieldSetTests.java index b18f52df5..b4242bf39 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/FieldSetTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/FieldSetTests.java @@ -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)); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapperTests.java index 99c155a9e..c6073091b 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapperTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapperTests.java @@ -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;