This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-25050-route-header-variables in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9491837d7f37423a6a4f1b032bb13b5946a9a39a Author: Claus Ibsen <[email protected]> AuthorDate: Sun Nov 1 20:04:58 2026 +0100 CAMEL-25050: camel-core - variableReceive into a route or group variable 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 b50ad720d318..04124a61bef8 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 @@ -319,6 +319,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.
