diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/file/builder/ResourcesItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/file/builder/ResourcesItemReaderBuilder.java new file mode 100644 index 0000000000..574d0ef3f0 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/file/builder/ResourcesItemReaderBuilder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2026 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.file.builder; + +import java.io.IOException; + +import org.jspecify.annotations.Nullable; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.ItemStreamSupport; +import org.springframework.batch.infrastructure.item.file.ResourcesItemReader; +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; + +/** + * A builder implementation for the {@link ResourcesItemReader}. + * + * @author Sanghyuk Jung + * @since 6.1 + * @see ResourcesItemReader + */ +public class ResourcesItemReaderBuilder { + + private Resource @Nullable [] resources; + + private @Nullable String filesPattern; + + private @Nullable String name; + + /** + * The name used to calculate the key within the {@link ExecutionContext}. + * @param name name of the reader instance + * @return this instance for method chaining. + * @see ItemStreamSupport#setName(String) + */ + public ResourcesItemReaderBuilder name(String name) { + this.name = name; + + return this; + } + + /** + * The array of resources that the {@link ResourcesItemReader} will serve up as items. + * @param resources the array of resources to use. + * @return this instance for method chaining. + * @see ResourcesItemReader#setResources(Resource[]) + */ + public ResourcesItemReaderBuilder resources(Resource... resources) { + this.resources = resources; + + return this; + } + + /** + * The location pattern of files that the {@link ResourcesItemReader} will serve up as + * 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 ResourcesItemReaderBuilder filesPattern(String filesPattern) { + this.filesPattern = filesPattern; + + return this; + } + + /** + * Builds the {@link ResourcesItemReader}. + * @return a {@link ResourcesItemReader} + */ + public ResourcesItemReader build() { + Assert.isTrue(this.resources != null || this.filesPattern != null, + "resources array or filesPattern is required."); + + ResourcesItemReader reader = new ResourcesItemReader(); + + 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); + } + } + + if (StringUtils.hasText(this.name)) { + reader.setName(this.name); + } + + return reader; + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/file/builder/ResourcesItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/file/builder/ResourcesItemReaderBuilderTests.java new file mode 100644 index 0000000000..4c388f963a --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/file/builder/ResourcesItemReaderBuilderTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 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.file.builder; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.file.ResourcesItemReader; +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author Sanghyuk Jung + */ +class ResourcesItemReaderBuilderTests { + + @Test + void testResources() throws Exception { + Resource resource1 = new ByteArrayResource("foo".getBytes()); + Resource resource2 = new ByteArrayResource("bar".getBytes()); + + ResourcesItemReader reader = new ResourcesItemReaderBuilder().resources(resource1, resource2) + .name("resourcesReader") + .build(); + + reader.open(new ExecutionContext()); + assertEquals(resource1, reader.read()); + assertEquals(resource2, reader.read()); + assertNull(reader.read()); + reader.close(); + } + + @Test + void testFilesPattern() throws Exception { + String basePath = new ClassPathResource("", this.getClass()).getFile().getPath(); + + ResourcesItemReader reader = new ResourcesItemReaderBuilder().filesPattern(basePath + "/resource?.txt") + .name("resourcesReader") + .build(); + + reader.open(new ExecutionContext()); + Set fileNames = new HashSet<>(); + for (Resource resource = reader.read(); resource != null; resource = reader.read()) { + fileNames.add(resource.getFilename()); + } + assertEquals(Set.of("resource1.txt", "resource2.txt"), fileNames); + reader.close(); + } + + @Test + void testName() throws Exception { + ResourcesItemReader reader = new ResourcesItemReaderBuilder().resources(new ByteArrayResource("foo".getBytes())) + .name("fooReader") + .build(); + + reader.open(new ExecutionContext()); + reader.read(); + ExecutionContext executionContext = new ExecutionContext(); + reader.update(executionContext); + assertEquals(1, executionContext.getInt("fooReader.COUNT")); + reader.close(); + } + + @Test + void testMissingResourcesAndFilesPattern() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> new ResourcesItemReaderBuilder().build()); + assertEquals("resources array or filesPattern is required.", exception.getMessage()); + } + +} diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/resource1.txt b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/resource1.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/resource1.txt @@ -0,0 +1 @@ +1 diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/resource2.txt b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/resource2.txt new file mode 100644 index 0000000000..0cfbf08886 --- /dev/null +++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/infrastructure/item/file/builder/resource2.txt @@ -0,0 +1 @@ +2