From 020d492120849beb18d99068d292ea8454ceb618 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sat, 14 Jun 2025 09:18:42 +0200 Subject: [PATCH] Add `MappingItemWriter` Signed-off-by: Stefano Cordio --- .../item/support/MappingItemWriter.java | 62 ++++++++++++++ .../MappingItemWriterIntegrationTests.java | 54 ++++++++++++ .../item/support/MappingItemWriterTests.java | 83 +++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/support/MappingItemWriter.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterIntegrationTests.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterTests.java diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/support/MappingItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/support/MappingItemWriter.java new file mode 100644 index 0000000000..0d035f83ce --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/support/MappingItemWriter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 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.infrastructure.item.support; + +import org.springframework.batch.infrastructure.item.Chunk; +import org.springframework.batch.infrastructure.item.ItemWriter; + +import java.util.function.Function; + +/** + * Adapts an {@link ItemWriter} accepting items of type {@link U} to one accepting items + * of type {@link T} by applying a mapping function to each item before writing. + *

+ * The {@code MappingItemWriter} is most useful when used in combination with a + * {@link org.springframework.batch.infrastructure.item.support.CompositeItemWriter}, + * where the mapping function in front of the downstream writer can be a getter of the + * input item or a more complex transformation logic. + *

+ * This adapter mimics the behavior of + * {@link java.util.stream.Collectors#mapping(Function, java.util.stream.Collector)}. + * + * @param the type of the input items + * @param type of items accepted by downstream item writer + * @author Stefano Cordio + * @since 6.0 + */ +public class MappingItemWriter implements ItemWriter { + + private final Function mapper; + + private final ItemWriter downstream; + + /** + * Create a new {@link MappingItemWriter}. + * @param mapper the mapping function to apply to the input items + * @param downstream the downstream item writer that accepts mapped items + */ + public MappingItemWriter(Function mapper, ItemWriter downstream) { + this.mapper = mapper; + this.downstream = downstream; + } + + @Override + public void write(Chunk chunk) throws Exception { + downstream.write(new Chunk<>(chunk.getItems().stream().map(mapper).toList())); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterIntegrationTests.java new file mode 100644 index 0000000000..4e637de183 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterIntegrationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025 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.infrastructure.item.support; + +import org.junit.jupiter.api.Test; +import org.springframework.batch.infrastructure.item.Chunk; +import org.springframework.batch.infrastructure.item.ItemWriter; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stefano Cordio + */ +class MappingItemWriterIntegrationTests { + + @Test + void testWithCompositeItemWriter() throws Exception { + // given + record Person(String name, int age) { + } + + ListItemWriter nameItemWriter = new ListItemWriter<>(); + ListItemWriter ageItemWriter = new ListItemWriter<>(); + + ItemWriter underTest = new CompositeItemWriter<>(List.of( // + new MappingItemWriter<>(Person::name, nameItemWriter), // + new MappingItemWriter<>(Person::age, ageItemWriter))); + + // when + underTest.write(Chunk.of(new Person("Foo", 42), new Person("Bar", 24))); + underTest.write(Chunk.of(new Person("Baz", 21), new Person("Qux", 12))); + + // then + assertThat(nameItemWriter.getWrittenItems()).containsExactly("Foo", "Bar", "Baz", "Qux"); + assertThat(ageItemWriter.getWrittenItems()).containsExactly(42, 24, 21, 12); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterTests.java new file mode 100644 index 0000000000..ee85a221c1 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/support/MappingItemWriterTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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.infrastructure.item.support; + +import org.junit.jupiter.api.Test; +import org.springframework.batch.infrastructure.item.Chunk; + +import java.util.function.Function; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Stefano Cordio + */ +class MappingItemWriterTests { + + @Test + void testWithMapperAcceptingItemSuperclass() throws Exception { + // given + Function mapper = Entity::name; + ListItemWriter downstream = mock(); + MappingItemWriter underTest = new MappingItemWriter<>(mapper, downstream); + + // when + underTest.write(Chunk.of(new Person("Foo", 42))); + + // then + verify(downstream).write(Chunk.of("Foo")); + } + + @Test + void testWithMapperProducingMappedItemSubclass() throws Exception { + // given + Function mapper = Person::name; + ListItemWriter downstream = mock(); + MappingItemWriter underTest = new MappingItemWriter<>(mapper, downstream); + + // when + underTest.write(Chunk.of(new Person("Foo", 42))); + + // then + verify(downstream).write(Chunk.of("Foo")); + } + + @Test + void testWithDownstreamAcceptingMappedItemSuperclass() throws Exception { + // given + Function mapper = Person::name; + ListItemWriter downstream = mock(); + MappingItemWriter underTest = new MappingItemWriter<>(mapper, downstream); + + // when + underTest.write(Chunk.of(new Person("Foo", 42))); + + // then + verify(downstream).write(Chunk.of("Foo")); + } + + private interface Entity { + + String name(); + + } + + private record Person(String name, int age) implements Entity { + } + +}