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 8aad3f56d373 CAMEL-25203: camel-java-io - read routes kept in local
variables; setHeaders with plain values can be dumped (#27153)
8aad3f56d373 is described below
commit 8aad3f56d373a9f578721a37fc0b92033ec9bddc
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 30 22:43:37 2026 +0200
CAMEL-25203: camel-java-io - read routes kept in local variables;
setHeaders with plain values can be dumped (#27153)
…Headers with plain values can be dumped
- the Java DSL parser builds a route kept in a local variable where it is
declared (RouteDefinition route = from(...)) and continues it on later
statements (route.to(...)), also a part of one (AggregateDefinition agg)
- setHeaders and setVariables with a String, number, boolean or char use the
constant language (with the value's type as the result type) instead of a
Java-only constant expression, so the Java, XML and YAML dumpers write
them;
the value at runtime is the same
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/model/ExpressionNodeHelper.java | 20 +++++++
.../apache/camel/model/SetHeadersDefinition.java | 5 +-
.../apache/camel/model/SetVariablesDefinition.java | 5 +-
.../camel/processor/SetHeadersProcessorTest.java | 27 +++++++++
.../org/apache/camel/java/in/ChainReplayer.java | 18 ++++++
.../org/apache/camel/java/in/JavaChainParser.java | 53 ++++++++++++++++-
.../org/apache/camel/java/in/LwJavaParserTest.java | 69 ++++++++++++++++++++++
7 files changed, 190 insertions(+), 7 deletions(-)
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/ExpressionNodeHelper.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/ExpressionNodeHelper.java
index 3a0f85781e19..ff988bf6cc8c 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/ExpressionNodeHelper.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/ExpressionNodeHelper.java
@@ -18,7 +18,9 @@ package org.apache.camel.model;
import org.apache.camel.Expression;
import org.apache.camel.Predicate;
+import org.apache.camel.builder.ExpressionBuilder;
import org.apache.camel.builder.ValueBuilder;
+import org.apache.camel.model.language.ConstantExpression;
import org.apache.camel.model.language.ExpressionDefinition;
import org.apache.camel.model.language.SimpleExpression;
import org.apache.camel.model.language.XPathExpression;
@@ -32,6 +34,24 @@ public final class ExpressionNodeHelper {
private ExpressionNodeHelper() {
}
+ /**
+ * A constant for a value given as data, such as in {@code
setHeaders(Map.of("foo", "ABC"))}: the constant language
+ * for a String, number, boolean or char (with its type as the result
type), so every DSL can write it; a Java
+ * constant expression for any other object.
+ */
+ public static Expression toConstantExpression(Object value) {
+ if (value instanceof String || value instanceof Number || value
instanceof Boolean
+ || value instanceof Character) {
+ ConstantExpression answer = new
ConstantExpression(value.toString());
+ if (!(value instanceof String)) {
+ answer.setResultType(value.getClass());
+ answer.setResultTypeName(value.getClass().getName());
+ }
+ return answer;
+ }
+ return ExpressionBuilder.constantExpression(value);
+ }
+
/**
* Determines which {@link ExpressionDefinition} describes the given
expression in the best possible way.
* <p/>
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/SetHeadersDefinition.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/SetHeadersDefinition.java
index cd352cfd9e71..7bf11872efed 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/SetHeadersDefinition.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/SetHeadersDefinition.java
@@ -29,7 +29,6 @@ import jakarta.xml.bind.annotation.XmlElementRef;
import jakarta.xml.bind.annotation.XmlRootElement;
import org.apache.camel.Expression;
-import org.apache.camel.builder.ExpressionBuilder;
import org.apache.camel.spi.Metadata;
/**
@@ -85,8 +84,8 @@ public class SetHeadersDefinition extends
ProcessorDefinition<SetHeadersDefiniti
throw new IllegalArgumentException("Keys must be Strings");
}
if (!(value instanceof Expression)) {
- // Assume it's a constant of some kind
- value = ExpressionBuilder.constantExpression(value);
+ // a constant: as the constant language when it is a plain value,
so every DSL can write it
+ value = ExpressionNodeHelper.toConstantExpression(value);
}
headers.add(new SetHeaderDefinition((String) key, (Expression) value));
}
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/SetVariablesDefinition.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/SetVariablesDefinition.java
index a8d9b0f2aa2a..9999dd12d910 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/SetVariablesDefinition.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/SetVariablesDefinition.java
@@ -29,7 +29,6 @@ import jakarta.xml.bind.annotation.XmlElementRef;
import jakarta.xml.bind.annotation.XmlRootElement;
import org.apache.camel.Expression;
-import org.apache.camel.builder.ExpressionBuilder;
import org.apache.camel.spi.Metadata;
/**
@@ -83,8 +82,8 @@ public class SetVariablesDefinition extends
ProcessorDefinition<SetVariablesDefi
throw new IllegalArgumentException("Keys must be Strings");
}
if (!(value instanceof Expression)) {
- // Assume it's a constant of some kind
- value = ExpressionBuilder.constantExpression(value);
+ // a constant: as the constant language when it is a plain value,
so every DSL can write it
+ value = ExpressionNodeHelper.toConstantExpression(value);
}
variables.add(new SetVariableDefinition((String) key, (Expression)
value));
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/SetHeadersProcessorTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/SetHeadersProcessorTest.java
index b7edde03ab45..4078526907b1 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/SetHeadersProcessorTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/SetHeadersProcessorTest.java
@@ -24,9 +24,14 @@ import org.apache.camel.Expression;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.language.constant.ConstantLanguage;
+import org.apache.camel.model.SetHeaderDefinition;
+import org.apache.camel.model.SetHeadersDefinition;
+import org.apache.camel.model.language.ConstantExpression;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
public class SetHeadersProcessorTest extends ContextTestSupport {
public static class HeaderBean {
@@ -102,6 +107,28 @@ public class SetHeadersProcessorTest extends
ContextTestSupport {
assertMockEndpointsSatisfied();
}
+ @Test
+ public void testPlainValuesKeepTheirTypeAndAreTheConstantLanguage() throws
Exception {
+ context.addRoutes(new RouteBuilder() {
+ public void configure() {
+ from("direct:startValues").routeId("values")
+ .setHeaders(Map.of("count", 5, "flag", true, "name",
"ABC")).to("mock:result");
+ }
+ });
+ expected.message(0).header("count").isEqualTo(5);
+ expected.message(0).header("count").isInstanceOf(Integer.class);
+ expected.message(0).header("flag").isInstanceOf(Boolean.class);
+ expected.message(0).header("name").isEqualTo("ABC");
+ template.sendBody("direct:startValues", body);
+ assertMockEndpointsSatisfied();
+
+ // the model has the constant language, which every DSL can write (a
Java-only expression before)
+ SetHeadersDefinition def = (SetHeadersDefinition)
context.getRouteDefinition("values").getOutputs().get(0);
+ for (SetHeaderDefinition h : def.getHeaders()) {
+ assertInstanceOf(ConstantExpression.class, h.getExpression(),
h.getName());
+ }
+ }
+
@Test
public void testUseMethod() throws Exception {
String hdrInBody = "foo,ABC,bar,XYZ";
diff --git
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java
index 256d7d7428ed..3ed1fc167b82 100644
---
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java
+++
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java
@@ -29,6 +29,7 @@ import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.IllegalFormatException;
import java.util.LinkedHashMap;
@@ -58,6 +59,7 @@ import org.apache.camel.java.in.JavaChainParser.ClassLit;
import org.apache.camel.java.in.JavaChainParser.ClassName;
import org.apache.camel.java.in.JavaChainParser.Concat;
import org.apache.camel.java.in.JavaChainParser.Lambda;
+import org.apache.camel.java.in.JavaChainParser.Local;
import org.apache.camel.java.in.JavaChainParser.New;
import org.apache.camel.java.in.JavaChainParser.Node;
import org.apache.camel.java.in.JavaChainParser.Null;
@@ -155,6 +157,8 @@ final class ChainReplayer {
private final EndpointDslResolver endpointDsl;
private final ConstantResolver constants;
private final List<JavaParseResult.Unresolved> unresolved = new
ArrayList<>();
+ /** The routes (or parts of one) kept in local variables of the builder
being replayed, by name. */
+ private final Map<String, Object> locals = new HashMap<>();
private final Set<String> resolving = new HashSet<>();
ChainReplayer(JavaChainParser.Source source) {
@@ -186,6 +190,7 @@ final class ChainReplayer {
List<Node> statements = source.builders().get(b);
builderParameter = source.builderParameters().get(b);
builder = new ReplayBuilder();
+ locals.clear();
for (Node statement : statements) {
if (statement instanceof Chain chain &&
configuresTheContext(chain)) {
// getContext().getComponent("sql",
SqlComponent.class).setDataSource(ds): not a route
@@ -195,6 +200,14 @@ final class ChainReplayer {
if (value instanceof Unknown u) {
report(u.node(), u.reason());
}
+ } else if (statement instanceof Local local) {
+ // RouteDefinition route = from("direct:a"): built here,
continued by route.to(...) later
+ Object value = evaluate(local.value());
+ if (value instanceof Unknown u) {
+ report(u.node(), u.reason());
+ } else {
+ locals.put(local.name(), value);
+ }
} else {
report(statement, "not a route chain");
}
@@ -630,6 +643,9 @@ final class ChainReplayer {
if (c.qualifier() != null && c.qualifier().equals(builderParameter)) {
// rb.simple(...) in a builder lambda: the builder
target = builder;
+ } else if (c.qualifier() != null && locals.containsKey(c.qualifier()))
{
+ // route.to(...) on a route kept in a local variable
+ target = locals.get(c.qualifier());
} else if (c.qualifier() != null) {
Class<?> type = loadable(c.qualifier());
if (type != null && STATIC_DSL.contains(type)) {
@@ -1398,6 +1414,8 @@ final class ChainReplayer {
return c.name() + "(" + String.join(", ",
c.args().stream().map(ChainReplayer::text).toList()) + ")";
} else if (node instanceof New n) {
return n.text();
+ } else if (node instanceof Local l) {
+ return l.name() + " = " + text(l.value());
} else if (node instanceof Lambda l) {
return l.text();
} else if (node instanceof Opaque o) {
diff --git
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
index fa4381fcb792..c90ba3fbc0f9 100644
---
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
+++
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
@@ -17,6 +17,7 @@
package org.apache.camel.java.in;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -90,6 +91,13 @@ final class JavaChainParser {
record New(String type, List<Node> args, boolean anonymous, String text,
int line) implements Node {
}
+ /**
+ * A local variable holding what a route builder entry returns, continued
by later statements:
+ * {@code RouteDefinition route = from("direct:a");} then {@code
route.to("mock:a");}.
+ */
+ record Local(String name, Node value, int line) implements Node {
+ }
+
/** A lambda or method reference. */
record Lambda(String text, int line) implements Node {
}
@@ -174,6 +182,8 @@ final class JavaChainParser {
for (int[] body : bodies) {
pos = body[0];
alias = body[2] >= 0 ? aliases.get(body[2]) : null;
+ // the locals of one configure() are not those of the next, as
the replay has them
+ routeLocals.clear();
List<Node> statements = new ArrayList<>();
statements(body[1], statements, constants);
builders.add(statements);
@@ -326,6 +336,11 @@ final class JavaChainParser {
skipStatement(end);
continue;
}
+ Local local = routeLocal();
+ if (local != null) {
+ out.add(local);
+ continue;
+ }
if (t.isIdent("final") || t.isIdent("var") ||
isLocalDeclaration()) {
if (t.isIdent("final")) {
pos++;
@@ -337,7 +352,8 @@ final class JavaChainParser {
if ((t.isIdent("this") || alias != null && t.isIdent(alias)) &&
at(1).is(".")) {
pos += 2;
}
- if (peek().kind() == Kind.IDENT && at(1).is("(")) {
+ if (peek().kind() == Kind.IDENT && (at(1).is("(") ||
routeLocals.contains(peek().text()) && at(1).is("."))) {
+ // a chain, or one continuing a route kept in a local
variable: route.to("mock:a")
Node chain = chainFrom();
// an expression lambda ends without a semicolon
if (peek().is(";") || pos >= end) {
@@ -360,6 +376,41 @@ final class JavaChainParser {
}
}
+ /** The local variables holding a route (or a part of one) in the builder
being read. */
+ private final Set<String> routeLocals = new HashSet<>();
+
+ /**
+ * {@code [final] Type name = from(...)...;}: a route kept in a local
variable, built where it is declared; null for
+ * any other statement.
+ */
+ private Local routeLocal() {
+ int p = pos;
+ if (tokens.get(p).isIdent("final")) {
+ p++;
+ }
+ Token type = tokens.get(p);
+ Token name = tokens.get(p + 1);
+ if (type.kind() != Kind.IDENT || name.kind() != Kind.IDENT ||
!tokens.get(p + 2).is("=")) {
+ return null;
+ }
+ Token entry = tokens.get(p + 3);
+ boolean fromLocal = routeLocals.contains(entry.text()) && tokens.get(p
+ 4).is(".");
+ if (entry.kind() != Kind.IDENT ||
!(BUILDER_ENTRIES.contains(entry.text()) && tokens.get(p + 4).is("(")
+ || fromLocal)) {
+ return null;
+ }
+ int start = pos;
+ pos = p + 3;
+ Node value = chainFrom();
+ if (!peek().is(";")) {
+ pos = start;
+ return null;
+ }
+ pos++;
+ routeLocals.add(name.text());
+ return new Local(name.text(), value, type.line());
+ }
+
/** Whether the statement is {@code Type name = ...;} with a simple type,
a local variable. */
private boolean isLocalDeclaration() {
return peek().kind() == Kind.IDENT && at(1).kind() == Kind.IDENT &&
at(2).is("=");
diff --git
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
index a83302815478..3aacf6067369 100644
---
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
+++
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.java.in;
import java.util.List;
+import org.apache.camel.model.AggregateDefinition;
import org.apache.camel.model.ChoiceDefinition;
import org.apache.camel.model.CircuitBreakerDefinition;
import org.apache.camel.model.DelayDefinition;
@@ -221,6 +222,8 @@ class LwJavaParserTest {
SplitDefinition split = (SplitDefinition) route.getOutputs().get(0);
SetHeadersDefinition headers = (SetHeadersDefinition)
split.getOutputs().get(0);
assertThat(headers.getHeaders()).extracting(SetHeaderDefinition::getName).containsExactly("foo",
"bar");
+ // a plain value is the constant language, which the dumpers write (it
was a Java-only expression)
+ assertThat(RoundTripTest.dump(route)).contains(".setHeaders(\"foo\",
constant(\"ABC\"), \"bar\", constant(\"XYZ\"))");
MarshalDefinition marshal = (MarshalDefinition)
split.getOutputs().get(1);
assertThat(((ZipDeflaterDataFormat)
marshal.getDataFormatType()).getCompressionLevel()).isEqualTo("9");
SetHeaderDefinition unit = (SetHeaderDefinition)
split.getOutputs().get(2);
@@ -264,6 +267,72 @@ class LwJavaParserTest {
assertThat(cb.getConfiguration()).isEqualTo("myConfig");
}
+ @Test
+ void aRouteKeptInALocalVariable() {
+ // a route built in steps: RouteDefinition route = from(...);
route.to(...)
+ JavaParseResult result = new LwJavaParser().parse("""
+ public class R extends RouteBuilder {
+ public void configure() {
+ RouteDefinition route =
from("direct:start").routeId("start");
+ route.split().body();
+ route.to("mock:split");
+
+ AggregateDefinition agg =
from("direct:joinBrothers").aggregate(header("type"), new MyStrategy());
+ agg.completionSize(2);
+ agg.to("mock:brothers");
+
+ from("direct:other").to("mock:other");
+ }
+ }
+ """);
+ List<RouteDefinition> routes = result.routes().getRoutes();
+ assertThat(routes).extracting(r -> r.getInput().getUri())
+ .containsExactly("direct:start", "direct:joinBrothers",
"direct:other");
+ RouteDefinition start = routes.get(0);
+ assertThat(start.getRouteId()).isEqualTo("start");
+
assertThat(start.getOutputs()).extracting(ProcessorDefinition::getShortName).containsExactly("split",
"to");
+ AggregateDefinition agg = (AggregateDefinition)
routes.get(1).getOutputs().get(0);
+ assertThat(agg.getCompletionSize()).isEqualTo("2");
+
assertThat(agg.getOutputs()).extracting(ProcessorDefinition::getShortName).containsExactly("to");
+ // only the strategy object is unknown
+
assertThat(result.unresolved()).extracting(JavaParseResult.Unresolved::text).containsExactly("new
MyStrategy()");
+ }
+
+ @Test
+ void theLocalsOfOneBuilderAreNotThoseOfTheNext() {
+ JavaParseResult result = new LwJavaParser().parse("""
+ public class Routes {
+ static class A extends RouteBuilder {
+ public void configure() {
+ RouteDefinition route = from("direct:a");
+ route.to("mock:a");
+ }
+ }
+ static class B extends RouteBuilder {
+ public void configure() {
+ route.to("mock:b");
+ from("direct:b").to("mock:b");
+ }
+ }
+ }
+ """);
+ List<RouteDefinition> routes = result.routes().getRoutes();
+ assertThat(routes).extracting(r ->
r.getInput().getUri()).containsExactly("direct:a", "direct:b");
+ // route.to("mock:b") in B is not a continuation of A's route
+ assertThat(routes.get(0).getOutputs()).hasSize(1);
+ }
+
+ @Test
+ void aLocalThatIsNotARouteStaysAValue() {
+ // Predicate god = ...: a value, used where the route refers to it,
not a route of its own
+ JavaParseResult result = new LwJavaParser().parse("""
+ Predicate god = header("type").isEqualTo("god");
+ from("direct:start").choice().when(god).to("mock:god").end();
+ """);
+ assertThat(result.routes().getRoutes()).hasSize(1);
+ assertThat(result.isComplete()).as("%s", result.unresolved()).isTrue();
+ }
+
@Test
void globalErrorHandlingAndRest() {
JavaParseResult result = new LwJavaParser().parse("""