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 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"; + } + } }