From 0ab2baef741d9ecb6de14dda15c09600f048759a Mon Sep 17 00:00:00 2001 From: Sanghyuk Jung Date: Sat, 1 Nov 2025 22:55:57 +0900 Subject: [PATCH] Add support for file patterns in MultiResourceItemReaderBuilder Resolves #5056 Signed-off-by: Sanghyuk Jung --- .../MultiResourceItemReaderBuilder.java | 40 ++++++++++++++++++- .../MultiResourceItemReaderBuilderTests.java | 24 ++++++++++- .../item/file/builder/test1.txt | 1 + .../item/file/builder/test2.txt | 1 + 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test1.txt create mode 100644 spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test2.txt diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilder.java index b503d10a6c..30c635181f 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilder.java @@ -16,6 +16,7 @@ package org.springframework.batch.infrastructure.item.file.builder; +import java.io.IOException; import java.util.Comparator; import org.jspecify.annotations.Nullable; @@ -25,6 +26,8 @@ import org.springframework.batch.infrastructure.item.file.MultiResourceItemReader; import org.springframework.batch.infrastructure.item.file.ResourceAwareItemReaderItemStream; import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -34,6 +37,7 @@ * @author Glenn Renfro * @author Drummond Dawson * @author Stefano Cordio + * @author Sanghyuk Jung * @since 4.0 * @see MultiResourceItemReader */ @@ -43,6 +47,8 @@ public class MultiResourceItemReaderBuilder { private Resource @Nullable [] resources; + private @Nullable String filesPattern; + private boolean strict = false; private @Nullable Comparator comparator; @@ -90,6 +96,19 @@ public MultiResourceItemReaderBuilder resources(Resource... resources) { return this; } + /** + * The location pattern of files that the {@link MultiResourceItemReader} will use to + * retrieve items. This is an Ant-style pattern that supports wildcards like `*`, `**` + * and `?`(for example `/data/*.csv`or `data/**\/user?.txt`). + * @param filesPattern the location pattern of files to use. + * @return this instance for method chaining. + */ + public MultiResourceItemReaderBuilder filesPattern(String filesPattern) { + this.filesPattern = filesPattern; + + return this; + } + /** * Establishes the delegate to use for reading the resources provided. * @param delegate reads items from single {@link Resource}. @@ -135,14 +154,31 @@ public MultiResourceItemReaderBuilder comparator(Comparator compara * @return a {@link MultiResourceItemReader} */ public MultiResourceItemReader build() { - Assert.notNull(this.resources, "resources array is required."); + Assert.isTrue(this.resources != null || this.filesPattern != null, + "resources array or filesPattern is required."); + Assert.notNull(this.delegate, "delegate is required."); if (this.saveState) { Assert.state(StringUtils.hasText(this.name), "A name is required when saveState is set to true."); } MultiResourceItemReader reader = new MultiResourceItemReader<>(this.delegate); - reader.setResources(this.resources); + + if (this.resources != null) { + reader.setResources(this.resources); + } + else if (this.filesPattern != null) { + ResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver(); + try { + Resource[] resources = patternResolver.getResources("file:" + this.filesPattern); + reader.setResources(resources); + } + catch (IOException e) { + throw new IllegalArgumentException("Unable to initialize resources by the pattern " + this.filesPattern, + e); + } + } + reader.setSaveState(this.saveState); reader.setStrict(this.strict); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilderTests.java index 9395cbf66a..cc11d3e30e 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/file/builder/MultiResourceItemReaderBuilderTests.java @@ -26,10 +26,13 @@ import org.springframework.batch.infrastructure.item.file.FlatFileItemReader; import org.springframework.batch.infrastructure.item.file.LineMapper; import org.springframework.batch.infrastructure.item.file.MultiResourceItemReader; +import org.springframework.batch.infrastructure.item.file.mapping.PassThroughLineMapper; import org.springframework.batch.infrastructure.item.sample.Foo; import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; @@ -79,7 +82,26 @@ void testNullDelegate() { void testNullResources() { Exception exception = assertThrows(IllegalArgumentException.class, () -> new MultiResourceItemReaderBuilder().delegate(mock(FlatFileItemReader.class)).build()); - assertEquals("resources array is required.", exception.getMessage()); + assertEquals("resources array or filesPattern is required.", exception.getMessage()); + } + + @Test + void testFilesPattern() throws Exception { + FlatFileItemReader delegate = new FlatFileItemReaderBuilder().name("textReader") + .lineMapper(new PassThroughLineMapper()) + .build(); + + String basePath = new ClassPathResource("", this.getClass()).getFile().getPath(); + MultiResourceItemReader reader = new MultiResourceItemReaderBuilder().delegate(delegate) + .filesPattern(basePath + "/test?.txt") + .name("multiFileReader") + .build(); + + reader.open(new ExecutionContext()); + assertEquals("1", reader.read()); + assertEquals("2", reader.read()); + assertNull(reader.read()); + reader.close(); } @Override diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test1.txt b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test1.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test1.txt @@ -0,0 +1 @@ +1 diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test2.txt b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test2.txt new file mode 100644 index 0000000000..0cfbf08886 --- /dev/null +++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/test2.txt @@ -0,0 +1 @@ +2