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 a0f6cee8324f CAMEL-25050: camel-core - variableReceive into a route or
group variable stores the header variables in that route or group (#28802)
a0f6cee8324f is described below
commit a0f6cee8324fccc254e3263ee1e38b19939d9a67
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 07:34:10 2026 +0100
CAMEL-25050: camel-core - variableReceive into a route or group variable
stores the header variables in that route or group (#28802)
…ble stores the header variables in that route or group
The header variables of route variable resp in route rs were stored as
header:rs:resp.status, which
the route repository split at the first colon into a route with the id
header. They could only be
read with route:header:rs:resp.status and were not removed with the route;
the same for groups.
They are now stored as rs:header:resp.status in the route (group) of the
variable, so
route:header:resp.status resolves to the current route,
route:rs:header:resp.status and
group:myGroup:header:resp.status work, and removing a route removes them
too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../processor/ToVariableReceiveHeadersTest.java | 56 +++++++++++++++++-----
.../org/apache/camel/support/ExchangeHelper.java | 27 ++++++++---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 16 +++++++
docs/user-manual/modules/ROOT/pages/variables.adoc | 13 +++++
4 files changed, 93 insertions(+), 19 deletions(-)
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
index d7cedd34a576..3c0b90da46bb 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/ToVariableReceiveHeadersTest.java
@@ -16,11 +16,14 @@
*/
package org.apache.camel.processor;
+import java.util.Set;
+
import org.apache.camel.ContextTestSupport;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.spi.BrowsableVariableRepository;
import org.apache.camel.spi.VariableRepositoryFactory;
+import org.apache.camel.support.GroupVariableRepository;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -70,14 +73,30 @@ class ToVariableReceiveHeadersTest extends
ContextTestSupport {
template.sendBody("direct:route", "Hello");
assertMockEndpointsSatisfied();
- // the header variables of route variable resp in route rs are stored
with the key header:rs:resp.<header>
+ // the header variables of route variable resp are stored in route rs,
as header:resp.<header>
assertEquals("Bye second", context.getVariable("route:rs:resp"));
- assertEquals(200, context.getVariable("route:header:rs:resp.status"));
- assertNull(context.getVariable("route:header:rs:resp.error"));
+ assertEquals(200, context.getVariable("route:rs:header:resp.status"));
+ assertNull(context.getVariable("route:rs:header:resp.error"));
// the header variables of another variable of the route, and of the
same variable in another route, are kept
- assertEquals("E42",
context.getVariable("route:header:rs:other.error"));
- assertEquals("E42",
context.getVariable("route:header:rs2:resp.error"));
+ assertEquals("E42",
context.getVariable("route:rs:header:other.error"));
+ assertEquals("E42",
context.getVariable("route:rs2:header:resp.error"));
assertEquals("Bye first", context.getVariable("route:rs2:resp"));
+ // no route with the id header
+ assertNull(context.getVariable("route:header:rs:resp.status"));
+ assertNoRouteOrGroupNamedHeader("route");
+ }
+
+ @Test
+ void testRemoveRouteRemovesHeaderVariables() throws Exception {
+ template.sendBody("direct:route2", "Hello");
+ assertEquals(500, context.getVariable("route:rs2:header:resp.status"));
+
+ context.getRouteController().stopRoute("rs2");
+ context.removeRoute("rs2");
+
+ assertNull(context.getVariable("route:rs2:resp"));
+ assertNull(context.getVariable("route:rs2:header:resp.status"));
+ assertNull(context.getVariable("route:rs2:header:resp.error"));
}
@Test
@@ -87,14 +106,27 @@ class ToVariableReceiveHeadersTest extends
ContextTestSupport {
template.sendBody("direct:group", "Hello");
assertMockEndpointsSatisfied();
- // the header variables of group variable myGroup:resp are stored with
the key header:myGroup:resp.<header>
+ // the header variables of group variable myGroup:resp are stored in
group myGroup, as header:resp.<header>
assertEquals("Bye second", context.getVariable("group:myGroup:resp"));
- assertEquals(200,
context.getVariable("group:header:myGroup:resp.status"));
- assertNull(context.getVariable("group:header:myGroup:resp.error"));
+ assertEquals(200,
context.getVariable("group:myGroup:header:resp.status"));
+ assertNull(context.getVariable("group:myGroup:header:resp.error"));
// the header variables of another variable of the group, and of the
same variable in another group, are kept
- assertEquals("E42",
context.getVariable("group:header:myGroup:other.error"));
- assertEquals("E42",
context.getVariable("group:header:otherGroup:resp.error"));
+ assertEquals("E42",
context.getVariable("group:myGroup:header:other.error"));
+ assertEquals("E42",
context.getVariable("group:otherGroup:header:resp.error"));
assertEquals("Bye first",
context.getVariable("group:otherGroup:resp"));
+ // no group with the id header
+ assertNull(context.getVariable("group:header:myGroup:resp.status"));
+ assertNoRouteOrGroupNamedHeader("group");
+ GroupVariableRepository repo = (GroupVariableRepository)
context.getCamelContextExtension()
+
.getContextPlugin(VariableRepositoryFactory.class).getVariableRepository("group");
+ assertEquals(Set.of("myGroup", "otherGroup"), repo.getGroupIds());
+ }
+
+ private void assertNoRouteOrGroupNamedHeader(String id) {
+ BrowsableVariableRepository repo = (BrowsableVariableRepository)
context.getCamelContextExtension()
+
.getContextPlugin(VariableRepositoryFactory.class).getVariableRepository(id);
+ assertFalse(repo.getVariables().keySet().stream().anyMatch(k ->
k.startsWith("header:")),
+ "header variables under the id header in " + id + "
repository: " + repo.getVariables().keySet());
}
private void assertNoErrorHeader(String id) {
@@ -126,7 +158,7 @@ class ToVariableReceiveHeadersTest extends
ContextTestSupport {
.setBody(constant("first")).toV("direct:svc", null,
"route:resp")
.setBody(constant("second")).toV("direct:svc", null,
"route:resp")
.setBody(simple(
-
"${variable.route:resp}|${variable.route:header:rs:resp.status}|${variable.route:header:rs:resp.error}"))
+
"${variable.route:resp}|${variable.route:header:resp.status}|${variable.route:header:resp.error}"))
.to("mock:route");
from("direct:route2").routeId("rs2")
@@ -138,7 +170,7 @@ class ToVariableReceiveHeadersTest extends
ContextTestSupport {
.setBody(constant("first")).toV("direct:svc", null,
"group:myGroup:resp")
.setBody(constant("second")).toV("direct:svc", null,
"group:myGroup:resp")
.setBody(simple(
-
"${variable.group:myGroup:resp}|${variable.group:header:myGroup:resp.status}|${variable.group:header:myGroup:resp.error}"))
+
"${variable.group:myGroup:resp}|${variable.group:myGroup:header:resp.status}|${variable.group:myGroup:header:resp.error}"))
.to("mock:group");
// only the reply to the first request has the error header
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
index bd23381a783e..e8db04bf1570 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
@@ -1234,7 +1234,8 @@ public final class ExchangeHelper {
public static String resolveVariableRepositoryName(Exchange exchange,
String name, String id) {
name = StringHelper.after(name, ":");
// special for route, where we need to enrich the name with current
route id if none given
- if ("route".equals(id) && !name.contains(":")) {
+ // (also for the header variables of a route variable, such as
header:resp.status)
+ if ("route".equals(id) && !hasRouteId(name)) {
String prefix = getAtRouteId(exchange);
if (prefix != null) {
name = prefix + ":" + name;
@@ -1246,6 +1247,13 @@ public final class ExchangeHelper {
return name;
}
+ private static boolean hasRouteId(String name) {
+ if (name.startsWith("header:")) {
+ name = name.substring(7);
+ }
+ return name.contains(":");
+ }
+
/**
* Sets the variable from the given message body and headers
*
@@ -1264,9 +1272,15 @@ public final class ExchangeHelper {
final VariableAware va = getVariableAware(exchange, repo);
// the headers are stored as header:name.key (in a route or group
repository the name is id:name, so the key
- // is header:id:name.key). Remove the headers of a previous message
stored in this variable, so a header that
- // this message does not have is not kept with its old value
- String prefix = "header:" + name + ".";
+ // is id:header:name.key, which keeps the headers in the route or
group of the variable). Remove the headers of
+ // a previous message stored in this variable, so a header that this
message does not have is not kept with
+ // its old value
+ String prefix;
+ if (repo instanceof RouteVariableRepository || repo instanceof
GroupVariableRepository) {
+ prefix = StringHelper.before(name, ":") + ":header:" +
StringHelper.after(name, ":") + ".";
+ } else {
+ prefix = "header:" + name + ".";
+ }
removeVariables(exchange, repo, prefix);
// set body and headers as variables
@@ -1280,9 +1294,8 @@ public final class ExchangeHelper {
}
private static void removeVariables(Exchange exchange, VariableRepository
repo, String prefix) {
- // the route and group repositories only scan the map of the id parsed
from the prefix instead of copying
- // the whole repository. For a prefix like header:<routeId>:<var>.
that id is always header (CAMEL-25050),
- // so the scan covers the shared header map of all routes or groups
+ // the route and group repositories only scan the map of the route or
group of the variable instead of copying
+ // the whole repository
if (repo instanceof RouteVariableRepository route) {
route.removeVariablesWithPrefix(prefix);
return;
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 8198a8b7ff92..170f1d8490c1 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
@@ -326,6 +326,22 @@ caller), put the EIP in a `doTry` with only a `doFinally`
in the called route, o
called route with `errorHandler(noErrorHandler())`, which turns it off for the
other steps of that route too. Neither
option brings back the redelivery of a whole routing slip or dynamic router.
+=== camel-core - variableReceive into a route or group variable stores the
header variables in that route or group
+
+With `variableReceive` (`toV`, `enrich`, `pollEnrich`, `fromV` and so on) into
a route variable or a group variable,
+the received headers are now stored in the route (group) of the variable.
Before, they were stored under a route
+(group) with the id `header`, so they could only be read with an internal
name, and were not removed with the route.
+
+[width="100%",cols="1m,2m,2m",options="header"]
+|===
+| Variable | Header variable before | Header variable now
+| route:resp (in route rs) | route:header:rs:resp.status |
route:header:resp.status or route:rs:header:resp.status
+| group:myGroup:resp | group:header:myGroup:resp.status |
group:myGroup:header:resp.status
+|===
+
+Inside a route, `route:header:resp.status` now means the header variable of
the route variable `resp` of the current
+route, not the variable `resp.status` of a route with the id `header`.
+
=== camel-util - FilterIterator.remove after hasNext throws
IllegalStateException
`FilterIterator.hasNext()` looks ahead in the iterator it wraps, so a
`remove()` after `next()` and then `hasNext()`
diff --git a/docs/user-manual/modules/ROOT/pages/variables.adoc
b/docs/user-manual/modules/ROOT/pages/variables.adoc
index b197f16eff80..0f5442146319 100644
--- a/docs/user-manual/modules/ROOT/pages/variables.adoc
+++ b/docs/user-manual/modules/ROOT/pages/variables.adoc
@@ -379,6 +379,19 @@ The *VariableReceive* works in a different mode. The idea
is that all the receiv
is not changed at all. The received body is stored in the variable, and the
received headers (transport headers etc.) are stored as read-only
headers as variables as well. The names of the variable is
`header:variableName.headerName`. For example, if the variable is `myVar` and
the header is `Content-Type`
then the header is stored as a variable with the full name
`header:myVar.Content-Type`.
+For a global variable the `header:` goes after the repository: `global:myVar`
has the header variable `global:header:myVar.Content-Type`.
+A route or group variable keeps its header variables in the same route or
group as the variable:
+
+[width="100%",cols="1m,2m",options="header"]
+|===
+| Variable | Header variable
+| route:myVar | route:header:myVar.Content-Type (in the current route)
+| route:myRoute:myVar | route:myRoute:header:myVar.Content-Type
+| group:myGroup:myVar | group:myGroup:header:myVar.Content-Type
+|===
+
+When a route is removed, its route variables are removed together with their
header variables.
+
When data is received into the same variable again, the header variables from
the previous time are removed first,
so the header variables are always the headers of the last received message.
This is done for exchange, global, route
and group variables. A custom variable repository that cannot be browsed keeps
the old header variables.