diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java new file mode 100644 index 0000000000..f512beb5e7 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 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 + * + * https://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.item.file.mapping; + +import java.lang.reflect.Constructor; + +import org.springframework.batch.item.file.transform.FieldSet; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.Assert; + +/** + * This is a {@link FieldSetMapper} that supports Java records mapping. + * It uses the record's canonical constructor to map components with the + * same name as tokens in the {@link FieldSet}. + * + * @param type of mapped items + * @author Mahmoud Ben Hassine + * @since 4.3 + */ +public class RecordFieldSetMapper implements FieldSetMapper { + + private final SimpleTypeConverter typeConverter = new SimpleTypeConverter(); + private final Constructor mappedConstructor; + private String[] constructorParameterNames; + private Class[] constructorParameterTypes; + + /** + * Create a new {@link RecordFieldSetMapper}. + * + * @param targetType type of mapped items + */ + public RecordFieldSetMapper(Class targetType) { + this(targetType, new DefaultConversionService()); + } + + /** + * Create a new {@link RecordFieldSetMapper}. + * + * @param targetType type of mapped items + * @param conversionService service to use to convert raw data to typed fields + */ + public RecordFieldSetMapper(Class< T> targetType, ConversionService conversionService) { + this.typeConverter.setConversionService(conversionService); + this.mappedConstructor = BeanUtils.getResolvableConstructor(targetType); + if (this.mappedConstructor.getParameterCount() > 0) { + this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); + this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + } + } + + @Override + public T mapFieldSet(FieldSet fieldSet) { + Assert.isTrue(fieldSet.getFieldCount() == this.constructorParameterNames.length, + "Fields count must be equal to record components count"); + Assert.isTrue(fieldSet.hasNames(), "Field names must specified"); + Object[] args = new Object[0]; + if (this.constructorParameterNames != null && this.constructorParameterTypes != null) { + args = new Object[this.constructorParameterNames.length]; + for (int i = 0; i < args.length; i++) { + String name = this.constructorParameterNames[i]; + Class type = this.constructorParameterTypes[i]; + args[i] = this.typeConverter.convertIfNecessary(fieldSet.readRawString(name), type); + } + } + return BeanUtils.instantiateClass(this.mappedConstructor, args); + } +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java new file mode 100644 index 0000000000..4a2b6a1bd5 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 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 + * + * https://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.item.file.mapping; + +import org.junit.Assert; +import org.junit.Test; + +import org.springframework.batch.item.file.transform.DefaultFieldSet; +import org.springframework.batch.item.file.transform.FieldSet; + +import static org.junit.Assert.fail; + +/** + * @author Mahmoud Ben Hassine + */ +public class RecordFieldSetMapperTests { + + @Test + public void testMapFieldSet() { + // given + RecordFieldSetMapper recordFieldSetMapper = new RecordFieldSetMapper<>(Person.class); + FieldSet fieldSet = new DefaultFieldSet(new String[]{"1", "foo"}, new String[] {"id", "name"}); + + // when + Person person = recordFieldSetMapper.mapFieldSet(fieldSet); + + // then + Assert.assertNotNull(person); + Assert.assertEquals(1, person.id()); + Assert.assertEquals("foo", person.name()); + } + + @Test + public void testMapFieldSetWhenFieldCountIsIncorrect() { + // given + RecordFieldSetMapper recordFieldSetMapper = new RecordFieldSetMapper<>(Person.class); + FieldSet fieldSet = new DefaultFieldSet(new String[]{"1"}, new String[] {"id"}); + + // when + try { + recordFieldSetMapper.mapFieldSet(fieldSet); + fail("Should fail when fields count is not equal to record components count"); + } catch (IllegalArgumentException e) { + // then + Assert.assertEquals("Fields count must be equal to record components count", e.getMessage()); + } + } + + @Test + public void testMapFieldSetWhenFieldNamesAreNotSpecified() { + // given + RecordFieldSetMapper recordFieldSetMapper = new RecordFieldSetMapper<>(Person.class); + FieldSet fieldSet = new DefaultFieldSet(new String[]{"1", "foo"}); + + // when + try { + recordFieldSetMapper.mapFieldSet(fieldSet); + fail("Should fail when field names are not specified"); + } catch (IllegalArgumentException e) { + // then + Assert.assertEquals("Field names must specified", e.getMessage()); + } + } + + public static class Person { // TODO change to record in v5 + private int id; + private String name; + + public Person(int id, String name) { + this.id = id; + this.name = name; + } + + public int id() { + return id; + } + + public String name() { + return name; + } + } +} \ No newline at end of file