diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java index 835abb3527..d07cbda99d 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2024 the original author or authors. + * Copyright 2006-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. @@ -34,9 +34,6 @@ * {@link #setItemCountLimitPerResource(int)}. Suffix creation can be customized with * {@link #setResourceSuffixCreator(ResourceSuffixCreator)}. *

- * Note that new resources are created only at chunk boundaries i.e. the number of items - * written into one resource is between the limit set by - *

* This writer will create an output file only when there are items to write, which means * there would be no empty file created if no items are passed (for example when all items * are filtered or skipped during the processing phase). @@ -45,6 +42,7 @@ * @param item type * @author Robert Kasanicky * @author Mahmoud Ben Hassine + * @author Henning Pƶttker */ public class MultiResourceItemWriter extends AbstractItemStreamItemWriter { @@ -74,22 +72,30 @@ public MultiResourceItemWriter() { @Override public void write(Chunk items) throws Exception { - if (!opened) { - File file = setResourceToDelegate(); - // create only if write is called - file.createNewFile(); - Assert.state(file.canWrite(), "Output resource " + file.getAbsolutePath() + " must be writable"); - delegate.open(new ExecutionContext()); - opened = true; - } - delegate.write(items); - currentResourceItemCount += items.size(); - if (currentResourceItemCount >= itemCountLimitPerResource) { - delegate.close(); - resourceIndex++; - currentResourceItemCount = 0; - setResourceToDelegate(); - opened = false; + int writtenItems = 0; + while (writtenItems < items.size()) { + if (!opened) { + File file = setResourceToDelegate(); + // create only if write is called + file.createNewFile(); + Assert.state(file.canWrite(), "Output resource " + file.getAbsolutePath() + " must be writable"); + delegate.open(new ExecutionContext()); + opened = true; + } + + int itemsToWrite = Math.min(itemCountLimitPerResource - currentResourceItemCount, + items.size() - writtenItems); + delegate.write(new Chunk(items.getItems().subList(writtenItems, writtenItems + itemsToWrite))); + currentResourceItemCount += itemsToWrite; + writtenItems += itemsToWrite; + + if (currentResourceItemCount >= itemCountLimitPerResource) { + delegate.close(); + resourceIndex++; + currentResourceItemCount = 0; + setResourceToDelegate(); + opened = false; + } } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java index ffe91317e9..ab23affa63 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2023 the original author or authors. + * Copyright 2008-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. @@ -78,22 +78,22 @@ void testBasicMultiResourceWriteScenario() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + + assertFileExistsAndContains(2, "34"); tested.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + + assertFileExistsAndContains(3, "5"); tested.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); + assertFileExistsAndContains(5, "9"); } @Test @@ -107,7 +107,7 @@ void testUpdateAfterDelegateClose() throws Exception { assertEquals(1, executionContext.getInt(tested.getExecutionContextKey("resource.index"))); tested.write(Chunk.of("1", "2", "3")); tested.update(executionContext); - assertEquals(0, executionContext.getInt(tested.getExecutionContextKey("resource.item.count"))); + assertEquals(1, executionContext.getInt(tested.getExecutionContextKey("resource.item.count"))); assertEquals(2, executionContext.getInt(tested.getExecutionContextKey("resource.index"))); } @@ -121,17 +121,22 @@ void testMultiResourceWriteScenarioWithFooter() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "3"); tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); + + assertFileExistsAndContains(2, "34f"); + + tested.write(Chunk.of("5")); + + assertFileExistsAndContains(3, "5"); tested.close(); - assertEquals("123f", readFile(part1)); - assertEquals("4f", readFile(part2)); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "34f"); + assertFileExistsAndContains(3, "5f"); } @@ -144,19 +149,18 @@ void testTransactionalMultiResourceWriteScenarioWithFooter() throws Exception { ResourcelessTransactionManager transactionManager = new ResourcelessTransactionManager(); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2", "3"))); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2"))); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); + assertFileExistsAndContains(1, "12f"); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("4"))); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("3"))); + + assertFileExistsAndContains(2, "3"); tested.close(); - assertEquals("123f", readFile(part1)); - assertEquals("4f", readFile(part2)); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "3f"); } @@ -168,27 +172,23 @@ void testRestart() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); - - tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); tested.update(executionContext); tested.close(); tested.open(executionContext); - tested.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + tested.write(Chunk.of("4")); - tested.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + assertFileExistsAndContains(2, "34"); + + tested.write(Chunk.of("5", "6", "7", "8", "9")); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); + assertFileExistsAndContains(5, "9"); } @Test @@ -201,27 +201,24 @@ void testRestartWithFooter() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123f", readFile(part1)); - - tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "3"); tested.update(executionContext); tested.close(); tested.open(executionContext); - tested.write(Chunk.of("5")); - assertEquals("45f", readFile(part2)); + tested.write(Chunk.of("4")); - tested.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789f", readFile(part3)); + assertFileExistsAndContains(2, "34f"); + + tested.write(Chunk.of("5", "6", "7", "8", "9")); + tested.close(); + + assertFileExistsAndContains(3, "56f"); + assertFileExistsAndContains(4, "78f"); + assertFileExistsAndContains(5, "9f"); } @Test @@ -233,24 +230,28 @@ void testTransactionalRestartWithFooter() throws Exception { ResourcelessTransactionManager transactionManager = new ResourcelessTransactionManager(); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2", "3"))); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2"))); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123f", readFile(part1)); + assertFileExistsAndContains(1, "12f"); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("4"))); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("3"))); + + assertFileExistsAndContains(2, "3"); tested.update(executionContext); tested.close(); tested.open(executionContext); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("5"))); - assertEquals("45f", readFile(part2)); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("4"))); + + assertFileExistsAndContains(2, "34f"); + } + + private void assertFileExistsAndContains(int index, String expected) throws Exception { + File part = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(index)); + assertTrue(part.exists()); + assertEquals(expected, readFile(part)); } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.java index 38760361c6..f6485adb80 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2022 the original author or authors. + * Copyright 2009-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. @@ -108,21 +108,26 @@ void multiResourceWritingWithRestart() throws Exception { tested.update(executionContext); tested.close(); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part2)); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part1)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part2)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part1)); tested.open(executionContext); tested.write(Chunk.of("5")); - - tested.write(Chunk.of("6", "7", "8", "9")); File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); assertTrue(part3.exists()); + tested.write(Chunk.of("6", "7", "8", "9")); + File part4 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(4)); + assertTrue(part4.exists()); + File part5 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(5)); + assertTrue(part5.exists()); + tested.close(); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part2)); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part3)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part3)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part4)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part5)); } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.java index 6eb75b9ed8..ce1ec6b8f4 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-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. @@ -84,22 +84,22 @@ void testBasicMultiResourceWriteScenario() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + + assertFileExistsAndContains(2, "34"); this.writer.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + + assertFileExistsAndContains(3, "5"); this.writer.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); + assertFileExistsAndContains(5, "9"); } @Test @@ -117,14 +117,12 @@ void testBasicDefaultSuffixCreator() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + simpleResourceSuffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); + assertFileExistsAndContains(1, "12", simpleResourceSuffixCreator); + assertFileExistsAndContains(2, "3", simpleResourceSuffixCreator); this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + simpleResourceSuffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + + assertFileExistsAndContains(2, "34", simpleResourceSuffixCreator); } @Test @@ -143,7 +141,7 @@ void testUpdateAfterDelegateClose() throws Exception { assertEquals(1, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.index"))); this.writer.write(Chunk.of("1", "2", "3")); this.writer.update(this.executionContext); - assertEquals(0, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.item.count"))); + assertEquals(1, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.item.count"))); assertEquals(2, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.index"))); } @@ -160,26 +158,21 @@ void testRestart() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); - - this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); this.writer.update(this.executionContext); this.writer.close(); this.writer.open(this.executionContext); - this.writer.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + this.writer.write(Chunk.of("4")); - this.writer.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + assertFileExistsAndContains(2, "34"); + + this.writer.write(Chunk.of("5", "6", "7", "8")); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); } @Test @@ -195,26 +188,23 @@ void testRestartNoSaveState() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); - - this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); this.writer.update(this.executionContext); this.writer.close(); this.writer.open(this.executionContext); - this.writer.write(Chunk.of("5")); - assertEquals("4", readFile(part2)); + this.writer.write(Chunk.of("4")); - this.writer.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part3.exists()); - assertEquals("56789", readFile(part3)); + assertFileExistsAndContains(2, "3"); + assertFileExistsAndContains(1, "4"); + + this.writer.write(Chunk.of("5", "6", "7", "8")); + + assertFileExistsAndContains(1, "45"); + assertFileExistsAndContains(2, "67"); + assertFileExistsAndContains(3, "8"); } @Test @@ -265,4 +255,15 @@ private String readFile(File f) throws Exception { return result.toString(); } + private void assertFileExistsAndContains(int index, String expected) throws Exception { + assertFileExistsAndContains(index, expected, this.suffixCreator); + } + + private void assertFileExistsAndContains(int index, String expected, ResourceSuffixCreator suffixCreator) + throws Exception { + File part = new File(this.file.getAbsolutePath() + suffixCreator.getSuffix(index)); + assertTrue(part.exists()); + assertEquals(expected, readFile(part)); + } + }