This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new e6d529b9d3e3 CAMEL-26122: camel-spring-batch - skip null headers and 
honour jobFromHeader=false (#28899)
e6d529b9d3e3 is described below

commit e6d529b9d3e332461daef0402d261d7702303692
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:38:24 2026 +0100

    CAMEL-26122: camel-spring-batch - skip null headers and honour 
jobFromHeader=false (#28899)
    
    …mHeader=false
    
    A message header with a null value was added as a String job parameter with 
a
    null value, which Spring Batch 6 rejects, so the job launch failed. Such
    headers are now skipped.
    
    The CamelSpringBatchJobName header chose the job to launch even with
    jobFromHeader=false. The producer now reads that header only when the
    endpoint has jobFromHeader=true. Docs and upgrade guide updated.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/catalog/components/spring-batch.json     |  4 +--
 .../camel/catalog/docs/spring-batch-component.adoc |  8 +++++-
 .../camel/component/spring/batch/spring-batch.json |  4 +--
 .../src/main/docs/spring-batch-component.adoc      |  8 +++++-
 .../spring/batch/SpringBatchConstants.java         |  3 ++-
 .../spring/batch/SpringBatchEndpoint.java          |  2 +-
 .../spring/batch/SpringBatchProducer.java          | 15 +++++++----
 .../spring/batch/SpringBatchEndpointTest.java      | 31 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    | 10 +++++++
 .../dsl/SpringBatchEndpointBuilderFactory.java     |  3 ++-
 10 files changed, 74 insertions(+), 14 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-batch.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-batch.json
index d1c8fbc4e8e9..a111a164230b 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-batch.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-batch.json
@@ -30,10 +30,10 @@
     "autowiredEnabled": { "index": 3, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "headers": {
-    "CamelSpringBatchJobName": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The name of the Batch Job to use", "constantName": 
"org.apache.camel.component.spring.batch.SpringBatchConstants#JOB_NAME" }
+    "CamelSpringBatchJobName": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The name of the Batch Job to use. Only used when the 
endpoint has jobFromHeader=true.", "constantName": 
"org.apache.camel.component.spring.batch.SpringBatchConstants#JOB_NAME" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "description": "The body is not 
read: the job parameters are the message headers (Date, Long and Double values 
as such, other values as text; a null header value fails)" },
+    "producerIn": { "carriers": [ "any" ], "description": "The body is not 
read: the job parameters are the message headers (Date, Long and Double values 
as such, other values as text; headers with a null value are skipped)" },
     "producerOut": { "carriers": [ "pojo" ], "javaType": 
"org.springframework.batch.core.job.JobExecution", "description": "The 
JobExecution of the launched job, with its status and exit status; the headers 
stay as they were" }
   },
   "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-batch-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-batch-component.adoc
index d3b20e6fe115..8a4e507ac76e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-batch-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-batch-component.adoc
@@ -74,7 +74,13 @@ Registry, then use it.
 All headers found in the message are passed to the `JobLauncher` as job
 parameters. `String`, `Long`, `Double` and `java.util.Date` values are
 copied to the `org.springframework.batch.core.JobParametersBuilder` -
-other data types are converted to Strings.
+other data types are converted to Strings. Headers with a `null` value are
+skipped, as Spring Batch does not accept a job parameter without a value.
+
+By default, the job to launch is the one named in the endpoint URI. To pick the
+job per message, set `jobFromHeader=true` on the endpoint and put the job name 
in
+the `CamelSpringBatchJobName` header. Without `jobFromHeader=true` that header
+is ignored when choosing the job.
 
 == Examples
 
diff --git 
a/components/camel-spring-parent/camel-spring-batch/src/generated/resources/META-INF/org/apache/camel/component/spring/batch/spring-batch.json
 
b/components/camel-spring-parent/camel-spring-batch/src/generated/resources/META-INF/org/apache/camel/component/spring/batch/spring-batch.json
index d1c8fbc4e8e9..a111a164230b 100644
--- 
a/components/camel-spring-parent/camel-spring-batch/src/generated/resources/META-INF/org/apache/camel/component/spring/batch/spring-batch.json
+++ 
b/components/camel-spring-parent/camel-spring-batch/src/generated/resources/META-INF/org/apache/camel/component/spring/batch/spring-batch.json
@@ -30,10 +30,10 @@
     "autowiredEnabled": { "index": 3, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "headers": {
-    "CamelSpringBatchJobName": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The name of the Batch Job to use", "constantName": 
"org.apache.camel.component.spring.batch.SpringBatchConstants#JOB_NAME" }
+    "CamelSpringBatchJobName": { "index": 0, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The name of the Batch Job to use. Only used when the 
endpoint has jobFromHeader=true.", "constantName": 
"org.apache.camel.component.spring.batch.SpringBatchConstants#JOB_NAME" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "description": "The body is not 
read: the job parameters are the message headers (Date, Long and Double values 
as such, other values as text; a null header value fails)" },
+    "producerIn": { "carriers": [ "any" ], "description": "The body is not 
read: the job parameters are the message headers (Date, Long and Double values 
as such, other values as text; headers with a null value are skipped)" },
     "producerOut": { "carriers": [ "pojo" ], "javaType": 
"org.springframework.batch.core.job.JobExecution", "description": "The 
JobExecution of the launched job, with its status and exit status; the headers 
stay as they were" }
   },
   "properties": {
diff --git 
a/components/camel-spring-parent/camel-spring-batch/src/main/docs/spring-batch-component.adoc
 
b/components/camel-spring-parent/camel-spring-batch/src/main/docs/spring-batch-component.adoc
index d3b20e6fe115..8a4e507ac76e 100644
--- 
a/components/camel-spring-parent/camel-spring-batch/src/main/docs/spring-batch-component.adoc
+++ 
b/components/camel-spring-parent/camel-spring-batch/src/main/docs/spring-batch-component.adoc
@@ -74,7 +74,13 @@ Registry, then use it.
 All headers found in the message are passed to the `JobLauncher` as job
 parameters. `String`, `Long`, `Double` and `java.util.Date` values are
 copied to the `org.springframework.batch.core.JobParametersBuilder` -
-other data types are converted to Strings.
+other data types are converted to Strings. Headers with a `null` value are
+skipped, as Spring Batch does not accept a job parameter without a value.
+
+By default, the job to launch is the one named in the endpoint URI. To pick the
+job per message, set `jobFromHeader=true` on the endpoint and put the job name 
in
+the `CamelSpringBatchJobName` header. Without `jobFromHeader=true` that header
+is ignored when choosing the job.
 
 == Examples
 
diff --git 
a/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchConstants.java
 
b/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchConstants.java
index a38766019473..3a3d23d34569 100644
--- 
a/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchConstants.java
+++ 
b/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchConstants.java
@@ -25,7 +25,8 @@ public final class SpringBatchConstants {
 
     public static final String DEFAULT_JOB_LAUNCHER_REF_NAME = "jobLauncher";
 
-    @Metadata(description = "The name of the Batch Job to use", javaType = 
"String")
+    @Metadata(description = "The name of the Batch Job to use. Only used when 
the endpoint has jobFromHeader=true.",
+              javaType = "String")
     public static final String JOB_NAME = "CamelSpringBatchJobName";
 
     private SpringBatchConstants() {
diff --git 
a/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchEndpoint.java
 
b/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchEndpoint.java
index f8ce01d07243..8aa5278e4925 100644
--- 
a/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchEndpoint.java
+++ 
b/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchEndpoint.java
@@ -41,7 +41,7 @@ import org.springframework.batch.core.launch.JobLauncher;
              remote = false, producerOnly = true, headersClass = 
SpringBatchConstants.class, category = { Category.WORKFLOW })
 @BodyTypes(producerIn = @BodyType(value = Carrier.ANY,
                                   description = "The body is not read: the job 
parameters are the message headers (Date, Long and"
-                                                + " Double values as such, 
other values as text; a null header value fails)"),
+                                                + " Double values as such, 
other values as text; headers with a null value are skipped)"),
            producerOut = @BodyType(value = Carrier.POJO, javaType = 
"org.springframework.batch.core.job.JobExecution",
                                    description = "The JobExecution of the 
launched job, with its status and exit status; the"
                                                  + " headers stay as they 
were"))
diff --git 
a/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchProducer.java
 
b/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchProducer.java
index b7a98a73924e..b327e9c4bca1 100644
--- 
a/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchProducer.java
+++ 
b/components/camel-spring-parent/camel-spring-batch/src/main/java/org/apache/camel/component/spring/batch/SpringBatchProducer.java
@@ -50,10 +50,17 @@ public class SpringBatchProducer extends DefaultProducer {
         this.jobRegistry = jobRegistry;
     }
 
+    @Override
+    public SpringBatchEndpoint getEndpoint() {
+        return (SpringBatchEndpoint) super.getEndpoint();
+    }
+
     @Override
     public void process(Exchange exchange) throws Exception {
         JobParameters jobParameters = 
prepareJobParameters(exchange.getIn().getHeaders());
-        String messageJobName = 
jobParameters.getString(SpringBatchConstants.JOB_NAME);
+        // the job name header is only used when the endpoint is configured 
with jobFromHeader=true
+        String messageJobName
+                = getEndpoint().isJobFromHeader() ? 
jobParameters.getString(SpringBatchConstants.JOB_NAME) : null;
 
         Job job2run = this.job;
 
@@ -83,7 +90,7 @@ public class SpringBatchProducer extends DefaultProducer {
     /**
      * Helper method converting the Camel message headers into the Spring 
Batch parameters map. Date, Long and Double
      * header values are converted to the appropriate types. All the other 
header values are converted to string
-     * representation.
+     * representation. Headers with a null value are skipped.
      *
      * @param  headers Camel message header to be converted
      * @return         Camel message headers converted into the Spring Batch 
parameters map
@@ -101,10 +108,8 @@ public class SpringBatchProducer extends DefaultProducer {
                 parametersBuilder.addDouble(headerKey, (Double) headerValue);
             } else if (headerValue != null) {
                 parametersBuilder.addString(headerKey, headerValue.toString());
-            } else {
-                // if the value is null we just put String with null value 
here to avoid the NPE
-                parametersBuilder.addString(headerKey, null);
             }
+            // a header with a null value is skipped, as Spring Batch does not 
accept a job parameter without a value
         }
         JobParameters jobParameters = parametersBuilder.toJobParameters();
         LOG.debug("Prepared parameters for Spring Batch job: {}", 
jobParameters);
diff --git 
a/components/camel-spring-parent/camel-spring-batch/src/test/java/org/apache/camel/component/spring/batch/SpringBatchEndpointTest.java
 
b/components/camel-spring-parent/camel-spring-batch/src/test/java/org/apache/camel/component/spring/batch/SpringBatchEndpointTest.java
index 231a75299072..169655158122 100644
--- 
a/components/camel-spring-parent/camel-spring-batch/src/test/java/org/apache/camel/component/spring/batch/SpringBatchEndpointTest.java
+++ 
b/components/camel-spring-parent/camel-spring-batch/src/test/java/org/apache/camel/component/spring/batch/SpringBatchEndpointTest.java
@@ -17,6 +17,8 @@
 package org.apache.camel.component.spring.batch;
 
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.EndpointInject;
@@ -41,11 +43,13 @@ import 
org.springframework.batch.core.job.parameters.JobParameters;
 import org.springframework.batch.core.launch.JobLauncher;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.BDDMockito.any;
 import static org.mockito.BDDMockito.eq;
 import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.never;
 import static org.mockito.BDDMockito.verify;
 import static org.mockito.BDDMockito.when;
 
@@ -156,6 +160,33 @@ public class SpringBatchEndpointTest extends 
CamelTestSupport {
         assertEquals(parameter, headerValue);
     }
 
+    @Test
+    public void shouldSkipNullHeadersWhenConvertingToJobParams() throws 
Exception {
+        // Given
+        Map<String, Object> headers = new HashMap<>();
+        headers.put("nullHeader", null);
+        headers.put("headerKey", "headerValue");
+
+        // When
+        template.sendBodyAndHeaders("direct:start", "Start the job, please.", 
headers);
+
+        // Then
+        ArgumentCaptor<JobParameters> jobParameters = 
ArgumentCaptor.forClass(JobParameters.class);
+        verify(jobLauncher).run(eq(job), jobParameters.capture());
+        assertNull(jobParameters.getValue().getParameter("nullHeader"));
+        assertEquals("headerValue", 
jobParameters.getValue().getString("headerKey"));
+    }
+
+    @Test
+    public void shouldIgnoreJobNameHeaderWhenJobFromHeaderIsFalse() throws 
Exception {
+        // When
+        template.sendBodyAndHeader("direct:start", "Start the job, please.", 
SpringBatchConstants.JOB_NAME, "dynamicMockjob");
+
+        // Then
+        verify(jobLauncher).run(eq(job), any(JobParameters.class));
+        verify(jobLauncher, never()).run(eq(dynamicMockjob), 
any(JobParameters.class));
+    }
+
     @Test
     public void shouldConvertDateHeadersToJobParams() throws Exception {
         // Given
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index b0657a286b5f..f50cb24f54d9 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -737,6 +737,16 @@ CamelContext of that provider. It now closes each of them 
only when no other `JC
 it returned the closed cache, whose operations throw `IllegalStateException`. 
This matters for code that calls a
 `JCacheManager` (for example one created by a custom `JCacheManagerFactory`) 
after it was closed.
 
+=== camel-spring-batch - the CamelSpringBatchJobName header needs 
jobFromHeader=true
+
+The producer now takes the job name from the `CamelSpringBatchJobName` header 
only when the endpoint has
+`jobFromHeader=true`. Before, the header chose which job was launched also 
with the default `jobFromHeader=false`,
+instead of the job named in the endpoint URI. Set `jobFromHeader=true` on the 
endpoint to keep picking the job from
+the header.
+
+A message header with a `null` value is no longer turned into a job parameter. 
Before, such a header failed the job
+launch, because Spring Batch does not accept a job parameter without a value.
+
 === camel-spring-rabbitmq - confirm=auto waits for simple publisher confirms
 
 With the default `confirm=auto`, an InOnly producer now also waits for the 
publisher confirm when the connection
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringBatchEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringBatchEndpointBuilderFactory.java
index 77f732bcd484..12a29a4885ca 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringBatchEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringBatchEndpointBuilderFactory.java
@@ -265,7 +265,8 @@ public interface SpringBatchEndpointBuilderFactory {
         public static final SpringBatchHeaderNameBuilder INSTANCE = new 
SpringBatchHeaderNameBuilder();
 
         /**
-         * The name of the Batch Job to use.
+         * The name of the Batch Job to use. Only used when the endpoint has
+         * jobFromHeader=true.
          * 
          * The option is a: {@code String} type.
          * 

Reply via email to