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("""

Reply via email to