diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java
index 08b192b90..340e334b3 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java
@@ -36,6 +36,8 @@ 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.core.convert.ConversionService;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.BindException;
@@ -45,7 +47,7 @@ import org.springframework.validation.DataBinder;
* {@link FieldSetMapper} implementation based on bean property paths. The
* {@link FieldSet} 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
+ * created and initialized either by referring to a prototype object by bean
* name in the enclosing BeanFactory, or by providing a class to instantiate
* reflectively.
*
@@ -63,7 +65,11 @@ import org.springframework.validation.DataBinder;
* can inject {@link 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 FieldSet} implementation.
+ * methods, or you can provide a custom {@link FieldSet} implementation.
+ * You can also use a {@link ConversionService} to convert to the desired type
+ * through the {@link #setConversionService(ConversionService) conversionService}
+ * property.
+ *
*
*
* Property name matching is "fuzzy" in the sense that it tolerates close
@@ -102,6 +108,10 @@ public class BeanWrapperFieldSetMapper extends DefaultPropertyEditorRegistrar
private boolean strict = true;
+ private ConversionService conversionService;
+
+ private boolean isCustomEditorsSet;
+
/*
* (non-Javadoc)
*
@@ -166,6 +176,7 @@ public class BeanWrapperFieldSetMapper extends DefaultPropertyEditorRegistrar
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(!this.isCustomEditorsSet || this.conversionService == null, "Both customEditor and conversionService cannot be specified together.");
}
/**
@@ -207,6 +218,9 @@ public class BeanWrapperFieldSetMapper extends DefaultPropertyEditorRegistrar
binder.setIgnoreUnknownFields(!this.strict);
initBinder(binder);
registerCustomEditors(binder);
+ if(this.conversionService != null) {
+ binder.setConversionService(this.conversionService);
+ }
return binder;
}
@@ -383,6 +397,31 @@ public class BeanWrapperFieldSetMapper extends DefaultPropertyEditorRegistrar
this.strict = strict;
}
+
+ /**
+ * Public setter for the 'conversionService' property.
+ * {@link #createBinder(Object)} will use it if not null.
+ *
+ * @param conversionService
+ */
+ public void setConversionService(ConversionService conversionService) {
+ this.conversionService = conversionService;
+ }
+
+ /**
+ * Specify the {@link PropertyEditor custom editors} to register.
+ *
+ *
+ * @param customEditors a map of Class to PropertyEditor (or class name to
+ * PropertyEditor).
+ * @see CustomEditorConfigurer#setCustomEditors(Map)
+ */
+ @Override
+ public void setCustomEditors(Map extends Object, ? extends PropertyEditor> customEditors) {
+ this.isCustomEditorsSet = true;
+ super.setCustomEditors(customEditors);
+ }
+
private static class DistanceHolder {
private final Class> cls;
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapperTests.java
index 25ab8271b..fb03c5a45 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapperTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapperTests.java
@@ -43,6 +43,10 @@ import org.springframework.beans.propertyeditors.PropertiesEditor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.lang.Nullable;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
@@ -51,27 +55,38 @@ public class BeanWrapperFieldSetMapperTests {
@Test
public void testNameAndTypeSpecified() throws Exception {
+ boolean errorCaught = false;
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
mapper.setTargetType(TestObject.class);
mapper.setPrototypeBeanName("foo");
try {
mapper.afterPropertiesSet();
}
- catch (IllegalStateException e) {
- // expected
+ catch (IllegalStateException ise) {
+ errorCaught = true;
+ assertEquals("Both name and type cannot be specified together.", ise.getMessage());
+ }
+ if (!errorCaught) {
+ fail();
}
}
@Test
public void testNameNorTypeSpecified() throws Exception {
+ boolean errorCaught = false;
BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
try {
mapper.afterPropertiesSet();
}
- catch (IllegalStateException e) {
- // expected
+ catch (IllegalStateException ise) {
+ errorCaught = true;
+ assertEquals("Either name or type must be provided.", ise.getMessage());
}
- }
+ if (!errorCaught) {
+ fail();
+ }
+
+}
@Test
public void testVanillaBeanCreatedFromType() throws Exception {
@@ -422,6 +437,78 @@ public class BeanWrapperFieldSetMapperTests {
assertEquals(7890.1, bean.getVarFloat(), 0.01);
}
+
+ @Test
+ public void testConversionWithTestConverter() throws Exception {
+
+ BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
+ mapper.setTargetType(TestObject.class);
+
+ FieldSet fieldSet = new DefaultFieldSet(new String[] { "SHOULD BE CONVERTED" }, new String[] { "varString" });
+
+ mapper.setConversionService(new TestConversion());
+ mapper.afterPropertiesSet();
+ TestObject bean = mapper.mapFieldSet(fieldSet);
+
+ assertEquals("Expecting the conversion to have returned \"CONVERTED\"", bean.getVarString(), "CONVERTED");
+ }
+
+ @Test
+ public void testDefaultConversion() throws Exception {
+
+ BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
+ mapper.setTargetType(TestObject.class);
+
+ final String sampleString = "myString";
+ Date date = new Date();
+ BigDecimal bigDecimal = new BigDecimal(12345L);
+ String dateString = date.toString();
+
+
+ FieldSet fieldSet = new DefaultFieldSet(new String[] { "12", "12345", "true", "Z", "123", "12345", "12345", "12", dateString, "12345", sampleString},
+ new String[] { "varInt", "varLong", "varBoolean", "varChar","varByte","varFloat", "varDouble", "varShort", "varDate", "varBigDecimal", "varString" });
+
+ mapper.setConversionService(new DefaultConversionService());
+ mapper.afterPropertiesSet();
+
+ TestObject bean = mapper.mapFieldSet(fieldSet);
+
+ assertEquals("Expected 12 for varInt", bean.getVarInt(), 12);
+ assertEquals("Expected 12345 for varLong", bean.getVarLong(), 12345L);
+ assertEquals("Expected true for varBoolean", bean.isVarBoolean(), true);
+ assertEquals("Expected Z for varChar", bean.getVarChar(), 'Z');
+ assertEquals("Expected A for varByte", bean.getVarByte(), 123);
+ assertEquals("Expected 12345 for varFloat", bean.getVarFloat(), 12345F, 1F);
+ assertEquals("Expected 12345 for varDouble", bean.getVarDouble(), 12345D, 1D);
+ assertEquals("Expected 12 for varShort", bean.getVarShort(), 12);
+ assertEquals("Expected currentDate for varDate", bean.getVarDate().toString(), dateString);
+ assertEquals("Expected 12345 for varBigDecimal", bean.getVarBigDecimal(), bigDecimal);
+ assertEquals("Expected " + sampleString + " for varString", bean.getVarString(), sampleString);
+
+ }
+
+ @Test
+ public void testConversionAndCustomEditor() throws Exception {
+
+ boolean errorCaught = false;
+ BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper();
+ mapper.setTargetType(TestObject.class);
+
+ mapper.setConversionService(new TestConversion());
+ mapper.setCustomEditors(Collections.singletonMap(Long.TYPE, new CustomNumberEditor(Long.class, NumberFormat
+ .getNumberInstance(), true)));
+ try {
+ mapper.afterPropertiesSet();
+ }
+ catch (IllegalStateException ise) {
+ errorCaught = true;
+ assertEquals("Both customEditor and conversionService cannot be specified together.", ise.getMessage());
+ }
+ if (!errorCaught) {
+ fail();
+ }
+ }
+
@Test
public void testBinderWithErrors() throws Exception {
@@ -732,4 +819,29 @@ public class BeanWrapperFieldSetMapperTests {
this.varInt = varInt;
}
}
+
+ public static class TestConversion implements ConversionService{
+
+ @Override
+ public boolean canConvert(@Nullable Class> sourceType, Class> targetType) {
+ return true;
+ }
+
+ @Override
+ public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public T convert(@Nullable Object source, Class targetType) {
+ return (T)"CONVERTED";
+ }
+
+ @Nullable
+ @Override
+ public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
+ return "CONVERTED";
+ }
+ }
}