This is an automated email from the ASF dual-hosted git repository.
Croway 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 45f4138f5e16 CAMEL-24797: camel-core - Splitter unwraps Exchange parts
like Message parts
45f4138f5e16 is described below
commit 45f4138f5e160590157cf6da67e49b5a4f0c3836
Author: croway <[email protected]>
AuthorDate: Thu Sep 17 16:15:35 2026 +0200
CAMEL-24797: camel-core - Splitter unwraps Exchange parts like Message parts
Splitting a List<Exchange> body (camel-kafka batching consumer, grouped
exchange aggregation strategy) now copies the body and headers of each
part into the child exchange, instead of making the part Exchange itself
the child body with no headers. The message is copied, not adopted, so
the exchanges owned by a batching consumer stay intact after the split.
Updates KafkaConsumerAsyncManualCommitIT and the Kafka batch headers docs
for the new shape, and documents the behaviour change in the 4.23 upgrade
guide.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
.../apache/camel/catalog/docs/kafka-component.adoc | 10 +-
.../camel-kafka/src/main/docs/kafka-component.adoc | 10 +-
.../KafkaConsumerAsyncManualCommitIT.java | 4 +-
.../java/org/apache/camel/processor/Splitter.java | 4 +
.../camel/processor/SplitListOfExchangesTest.java | 107 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 25 +++++
6 files changed, 148 insertions(+), 12 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kafka-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kafka-component.adoc
index ba38a3af68de..9a9b669a78ec 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kafka-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kafka-component.adoc
@@ -761,16 +761,16 @@ of time. Therefore, it's recommended to keep this value
as default.
==== Batch Headers
The exchange carrying the batch also exposes the record metadata headers that
every record in the batch agrees on:
-`CamelKafkaTopic` and `CamelKafkaPartition`. This makes it possible to read
them before splitting the batch, for
-example to store the topic in a variable and reuse it after the split:
+`CamelKafkaTopic` and `CamelKafkaPartition`. This makes it possible to read
them on the batch itself, before
+splitting it. When the batch is split, each child exchange carries the body
and the headers of its own record:
-._Java-only: reading the topic from the batch before splitting_
+._Java-only: reading the topic from the batch and the offset from each record_
[source,java]
----
from("kafka:topic?groupId=myGroup&batching=true&maxPollRecords=10")
- .setVariable("topic", header(KafkaConstants.TOPIC))
+ .log("Received a batch of ${body.size} records from
${header.CamelKafkaTopic}")
.split(body())
- .log("Record from ${variable.topic}")
+ .log("Record ${body} at offset ${header.CamelKafkaOffset}")
.end();
----
diff --git a/components/camel-kafka/src/main/docs/kafka-component.adoc
b/components/camel-kafka/src/main/docs/kafka-component.adoc
index ba38a3af68de..9a9b669a78ec 100644
--- a/components/camel-kafka/src/main/docs/kafka-component.adoc
+++ b/components/camel-kafka/src/main/docs/kafka-component.adoc
@@ -761,16 +761,16 @@ of time. Therefore, it's recommended to keep this value
as default.
==== Batch Headers
The exchange carrying the batch also exposes the record metadata headers that
every record in the batch agrees on:
-`CamelKafkaTopic` and `CamelKafkaPartition`. This makes it possible to read
them before splitting the batch, for
-example to store the topic in a variable and reuse it after the split:
+`CamelKafkaTopic` and `CamelKafkaPartition`. This makes it possible to read
them on the batch itself, before
+splitting it. When the batch is split, each child exchange carries the body
and the headers of its own record:
-._Java-only: reading the topic from the batch before splitting_
+._Java-only: reading the topic from the batch and the offset from each record_
[source,java]
----
from("kafka:topic?groupId=myGroup&batching=true&maxPollRecords=10")
- .setVariable("topic", header(KafkaConstants.TOPIC))
+ .log("Received a batch of ${body.size} records from
${header.CamelKafkaTopic}")
.split(body())
- .log("Record from ${variable.topic}")
+ .log("Record ${body} at offset ${header.CamelKafkaOffset}")
.end();
----
diff --git
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/integration/KafkaConsumerAsyncManualCommitIT.java
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/integration/KafkaConsumerAsyncManualCommitIT.java
index 0d45195830c2..02bf9e90c9a4 100644
---
a/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/integration/KafkaConsumerAsyncManualCommitIT.java
+++
b/components/camel-kafka/src/test/java/org/apache/camel/component/kafka/integration/KafkaConsumerAsyncManualCommitIT.java
@@ -93,8 +93,8 @@ public class KafkaConsumerAsyncManualCommitIT extends
BaseKafkaTestSupport {
.aggregationStrategy(AggregationStrategies.groupedExchange())
.split().body()
.process(e -> {
- KafkaManualCommit manual =
e.getMessage().getBody(Exchange.class)
-
.getMessage().getHeader(KafkaConstants.MANUAL_COMMIT, KafkaManualCommit.class);
+ KafkaManualCommit manual
+ =
e.getMessage().getHeader(KafkaConstants.MANUAL_COMMIT, KafkaManualCommit.class);
assertNotNull(manual);
try {
diff --git
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
index 78a89bdc33ba..050872386d75 100644
---
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
+++
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Splitter.java
@@ -422,6 +422,10 @@ public class Splitter extends MulticastProcessor {
}
if (part instanceof Message message) {
newExchange.setIn(message);
+ } else if (part instanceof Exchange partExchange) {
+ // copy (do not adopt) the message, so the
exchange owning the part, such as the batch
+ // exchange of a batching consumer, is left
untouched when the child exchange completes
+
newExchange.getIn().copyFrom(partExchange.getMessage());
} else {
Message in = newExchange.getIn();
in.setBody(part);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/SplitListOfExchangesTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitListOfExchangesTest.java
new file mode 100644
index 000000000000..5510530c2116
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/SplitListOfExchangesTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.camel.processor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.Message;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+import org.apache.camel.impl.engine.PooledProcessorExchangeFactory;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Splitting a body of type {@code List<Exchange>}, as produced by batching
consumers and the grouped exchange
+ * aggregation strategy, should unwrap each part into the child exchange, like
splitting a {@code List<Message>} does.
+ * Exchange pooling is enabled because a pooled child exchange resets its
message on completion, which would wipe the
+ * parts if their message was adopted instead of copied.
+ */
+public class SplitListOfExchangesTest extends ContextTestSupport {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext camelContext = super.createCamelContext();
+ ExtendedCamelContext ecc = camelContext.getCamelContextExtension();
+ ecc.setExchangeFactory(new PooledExchangeFactory());
+ ecc.setProcessorExchangeFactory(new PooledProcessorExchangeFactory());
+ return camelContext;
+ }
+
+ @Test
+ public void testSplitListOfExchanges() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:split");
+ mock.expectedBodiesReceived("A", "B", "C");
+ mock.expectedHeaderValuesReceivedInAnyOrder("foo", 1, 2, 3);
+
+ List<Exchange> parts = createParts();
+ template.sendBody("direct:start", parts);
+
+ assertMockEndpointsSatisfied();
+
+ for (Exchange child : mock.getReceivedExchanges()) {
+ assertEquals("bar", child.getMessage().getHeader("common"));
+ // the part is unwrapped, the child body is not the part exchange
itself
+ assertNull(child.getMessage().getBody(Exchange.class));
+ // exchange properties of the part are not carried over (same as
for Message parts)
+ assertNull(child.getProperty("partProperty"));
+ }
+
+ // the owner of the parts (e.g. a batching consumer committing after
the route) still sees them intact
+ for (int i = 0; i < parts.size(); i++) {
+ Message part = parts.get(i).getMessage();
+ assertEquals(List.of("A", "B", "C").get(i), part.getBody());
+ assertEquals(i + 1, part.getHeader("foo"));
+ assertEquals("bar", part.getHeader("common"));
+ }
+ }
+
+ private List<Exchange> createParts() {
+ List<Exchange> parts = new ArrayList<>();
+ int i = 1;
+ for (String body : List.of("A", "B", "C")) {
+ Exchange part = new DefaultExchange(context);
+ Message message = part.getMessage();
+ message.setBody(body);
+ message.setHeader("foo", i++);
+ message.setHeader("common", "bar");
+ part.setProperty("partProperty", "notCopied");
+ parts.add(part);
+ }
+ return parts;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ from("direct:start")
+ .split(body())
+ .to("mock:split");
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 943072cffb7d..ea7421392502 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -478,6 +478,31 @@ The XML schemas (`camel-spring.xsd`, `camel-xml-io.xsd`)
and the YAML schema no
ran. A bean that is neither scripted nor built still needs a `type`, and the
error for a missing one
is now an `IllegalArgumentException` naming the bean instead of a
`NullPointerException`.
+=== camel-core - Splitter unwraps Exchange parts
+
+The Splitter EIP now unwraps parts of type `org.apache.camel.Exchange`, the
same way it already unwrapped parts of
+type `org.apache.camel.Message`. When splitting a body of type
`List<Exchange>`, as produced by the camel-kafka
+batching consumer (`batching=true`) and by the grouped exchange aggregation
strategy, each child exchange now
+carries the body and the headers of the corresponding part. Previously the
child body was the part `Exchange`
+itself, with no headers.
+
+Code that read the part from the child body must be updated, for example when
performing a Kafka manual commit
+after a split:
+
+[source,java]
+----
+// before
+KafkaManualCommit manual = exchange.getMessage().getBody(Exchange.class)
+ .getMessage().getHeader(KafkaConstants.MANUAL_COMMIT,
KafkaManualCommit.class);
+
+// now
+KafkaManualCommit manual =
exchange.getMessage().getHeader(KafkaConstants.MANUAL_COMMIT,
KafkaManualCommit.class);
+----
+
+The message of the part is copied into the child exchange, so the exchanges in
the original list are left
+untouched and remain usable after the split, for example by the batching
consumer that owns them. Exchange
+properties of the part are not carried over to the child exchange, as is
already the case for `Message` parts.
+
=== Component deprecation
==== camel-minio