Skip to content

Commit 615a4ae

Browse files
committed
Add initial support for Java 14 records mapping
Issue #3693
1 parent 65d31bf commit 615a4ae

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.item.file.mapping;
17+
18+
import java.lang.reflect.Constructor;
19+
20+
import org.springframework.batch.item.file.transform.FieldSet;
21+
import org.springframework.beans.BeanUtils;
22+
import org.springframework.beans.SimpleTypeConverter;
23+
import org.springframework.core.convert.ConversionService;
24+
import org.springframework.core.convert.support.DefaultConversionService;
25+
import org.springframework.util.Assert;
26+
27+
/**
28+
* This is a {@link FieldSetMapper} that supports Java records mapping.
29+
* It uses the record's canonical constructor to map components with the
30+
* same name as tokens in the {@link FieldSet}.
31+
*
32+
* @param <T> type of mapped items
33+
* @author Mahmoud Ben Hassine
34+
* @since 4.3
35+
*/
36+
public class RecordFieldSetMapper<T> implements FieldSetMapper<T> {
37+
38+
private final SimpleTypeConverter typeConverter = new SimpleTypeConverter();
39+
private final Constructor<T> mappedConstructor;
40+
private String[] constructorParameterNames;
41+
private Class<?>[] constructorParameterTypes;
42+
43+
/**
44+
* Create a new {@link RecordFieldSetMapper}.
45+
*
46+
* @param targetType type of mapped items
47+
*/
48+
public RecordFieldSetMapper(Class<T> targetType) {
49+
this(targetType, new DefaultConversionService());
50+
}
51+
52+
/**
53+
* Create a new {@link RecordFieldSetMapper}.
54+
*
55+
* @param targetType type of mapped items
56+
* @param conversionService service to use to convert raw data to typed fields
57+
*/
58+
public RecordFieldSetMapper(Class< T> targetType, ConversionService conversionService) {
59+
this.typeConverter.setConversionService(conversionService);
60+
this.mappedConstructor = BeanUtils.getResolvableConstructor(targetType);
61+
if (this.mappedConstructor.getParameterCount() > 0) {
62+
this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor);
63+
this.constructorParameterTypes = this.mappedConstructor.getParameterTypes();
64+
}
65+
}
66+
67+
@Override
68+
public T mapFieldSet(FieldSet fieldSet) {
69+
Assert.isTrue(fieldSet.getFieldCount() == this.constructorParameterNames.length,
70+
"Fields count must be equal to record components count");
71+
Assert.isTrue(fieldSet.hasNames(), "Field names must specified");
72+
Object[] args = new Object[0];
73+
if (this.constructorParameterNames != null && this.constructorParameterTypes != null) {
74+
args = new Object[this.constructorParameterNames.length];
75+
for (int i = 0; i < args.length; i++) {
76+
String name = this.constructorParameterNames[i];
77+
Class<?> type = this.constructorParameterTypes[i];
78+
args[i] = this.typeConverter.convertIfNecessary(fieldSet.readRawString(name), type);
79+
}
80+
}
81+
return BeanUtils.instantiateClass(this.mappedConstructor, args);
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.item.file.mapping;
17+
18+
import org.junit.Assert;
19+
import org.junit.Test;
20+
21+
import org.springframework.batch.item.file.transform.DefaultFieldSet;
22+
import org.springframework.batch.item.file.transform.FieldSet;
23+
24+
import static org.junit.Assert.fail;
25+
26+
/**
27+
* @author Mahmoud Ben Hassine
28+
*/
29+
public class RecordFieldSetMapperTests {
30+
31+
@Test
32+
public void testMapFieldSet() {
33+
// given
34+
RecordFieldSetMapper<Person> recordFieldSetMapper = new RecordFieldSetMapper<>(Person.class);
35+
FieldSet fieldSet = new DefaultFieldSet(new String[]{"1", "foo"}, new String[] {"id", "name"});
36+
37+
// when
38+
Person person = recordFieldSetMapper.mapFieldSet(fieldSet);
39+
40+
// then
41+
Assert.assertNotNull(person);
42+
Assert.assertEquals(1, person.id());
43+
Assert.assertEquals("foo", person.name());
44+
}
45+
46+
@Test
47+
public void testMapFieldSetWhenFieldCountIsIncorrect() {
48+
// given
49+
RecordFieldSetMapper<Person> recordFieldSetMapper = new RecordFieldSetMapper<>(Person.class);
50+
FieldSet fieldSet = new DefaultFieldSet(new String[]{"1"}, new String[] {"id"});
51+
52+
// when
53+
try {
54+
recordFieldSetMapper.mapFieldSet(fieldSet);
55+
fail("Should fail when fields count is not equal to record components count");
56+
} catch (IllegalArgumentException e) {
57+
// then
58+
Assert.assertEquals("Fields count must be equal to record components count", e.getMessage());
59+
}
60+
}
61+
62+
@Test
63+
public void testMapFieldSetWhenFieldNamesAreNotSpecified() {
64+
// given
65+
RecordFieldSetMapper<Person> recordFieldSetMapper = new RecordFieldSetMapper<>(Person.class);
66+
FieldSet fieldSet = new DefaultFieldSet(new String[]{"1", "foo"});
67+
68+
// when
69+
try {
70+
recordFieldSetMapper.mapFieldSet(fieldSet);
71+
fail("Should fail when field names are not specified");
72+
} catch (IllegalArgumentException e) {
73+
// then
74+
Assert.assertEquals("Field names must specified", e.getMessage());
75+
}
76+
}
77+
78+
public static class Person { // TODO change to record in v5
79+
private int id;
80+
private String name;
81+
82+
public Person(int id, String name) {
83+
this.id = id;
84+
this.name = name;
85+
}
86+
87+
public int id() {
88+
return id;
89+
}
90+
91+
public String name() {
92+
return name;
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)