This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-25255
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 52aafa78416da9d24e6170e457959cec6102a9bf
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 08:59:32 2026 +0200

    CAMEL-25255: camel-java-io - the Java writer keeps the route error handler, 
xpath namespaces and the kind of language
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/model/ResequenceDefinition.java   |  3 +-
 .../org/apache/camel/model/RouteDefinition.java    |  6 ++
 .../camel/java/out/JavaDslModelWriterSupport.java  | 81 +++++++++++++++-------
 .../org/apache/camel/java/in/RoundTripTest.java    | 53 ++++++++++++++
 .../apache/camel/java/out/JavaDslCompileTest.java  |  1 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    | 20 ++++++
 6 files changed, 139 insertions(+), 25 deletions(-)

diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/ResequenceDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/ResequenceDefinition.java
index a66234e573c5..ab7c64b3af12 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/ResequenceDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/ResequenceDefinition.java
@@ -147,7 +147,8 @@ public class ResequenceDefinition extends 
OutputDefinition<ResequenceDefinition>
      * @return         the builder
      */
     public ResequenceDefinition timeout(long timeout) {
-        return timeout(Duration.ofMillis(timeout));
+        // the millis as given (not 2s0ms for 2000), so the model reads as the 
route was written
+        return timeout(String.valueOf(timeout));
     }
 
     /**
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
index 3193295489d7..3e94918b6868 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinition.java
@@ -580,6 +580,12 @@ public class RouteDefinition extends 
OutputDefinition<RouteDefinition>
      */
     public RouteDefinition errorHandler(ErrorHandlerFactory 
errorHandlerBuilder) {
         setErrorHandlerFactory(errorHandlerBuilder);
+        // kept in the model as well (as the xml and yaml dsl have it), so 
dumping the route writes its error handler:
+        // as its definition (deadLetterChannel() gives a 
DeadLetterChannelBuilder, which the model writers do not know)
+        ErrorHandlerDefinition def = new ErrorHandlerDefinition();
+        def.setErrorHandlerType(errorHandlerBuilder instanceof 
CopyableDefinition<?> copyable
+                && copyable.copyDefinition() instanceof ErrorHandlerFactory 
copy ? copy : errorHandlerBuilder);
+        this.errorHandler = def;
         // we are now using a route scoped error handler
         contextScopedErrorHandler = false;
         return this;
diff --git 
a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
 
b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
index 43a77f1eaba4..1a83c60b8693 100644
--- 
a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
+++ 
b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
@@ -24,9 +24,11 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.StringJoiner;
 import java.util.function.BiConsumer;
 
 import org.apache.camel.builder.ExpressionClause;
+import org.apache.camel.builder.LanguageBuilderFactory;
 import org.apache.camel.model.BeanDefinition;
 import org.apache.camel.model.BeanFactoryDefinition;
 import org.apache.camel.model.ConvertBodyDefinition;
@@ -81,7 +83,9 @@ import org.apache.camel.model.language.ExpressionDefinition;
 import org.apache.camel.model.language.HeaderExpression;
 import org.apache.camel.model.language.JqExpression;
 import org.apache.camel.model.language.JsonPathExpression;
+import org.apache.camel.model.language.LanguageExpression;
 import org.apache.camel.model.language.MethodCallExpression;
+import org.apache.camel.model.language.NamespaceAwareExpression;
 import org.apache.camel.model.language.RefExpression;
 import org.apache.camel.model.language.SimpleExpression;
 import org.apache.camel.model.language.SingleInputTypedExpressionDefinition;
@@ -262,12 +266,6 @@ public abstract class JavaDslModelWriterSupport {
             }
         }
 
-        // extract inlined error handler — RouteBuilder-level in Java DSL
-        if (def.getErrorHandler() != null) {
-            writeErrorHandler(sb, def.getErrorHandler());
-            handledAttributes.add("errorHandler");
-        }
-
         if (def.getInput() != null) {
             
sb.append("from(").append(quote(def.getInput().getUri())).append(")");
             if (sourceLocation) {
@@ -293,6 +291,12 @@ public abstract class JavaDslModelWriterSupport {
             handledAttributes.add("routeProperty");
             handledAttributes.add("routeProperties");
         }
+        // the error handler of the route only: errorHandler() of the route 
builder would apply to all its routes
+        if (def.getErrorHandler() != null) {
+            
sb.append(NL).append(indent()).append(".errorHandler(").append(errorHandlerDsl(def.getErrorHandler()))
+                    .append(")");
+        }
+        handledAttributes.add("errorHandler");
         indentLevel--;
         doWriteRouteDefinition(sb, def);
         indentLevel++;
@@ -363,6 +367,10 @@ public abstract class JavaDslModelWriterSupport {
             }
             handledAttributes.add("routeProperty");
             handledAttributes.add("routeProperties");
+            if (route.getErrorHandler() != null) {
+                sb.append(NL).append(indent()).append(".errorHandler(")
+                        
.append(errorHandlerDsl(route.getErrorHandler())).append(")");
+            }
             handledAttributes.add("errorHandler");
             indentLevel--;
             doWriteRouteDefinition(sb, route);
@@ -482,18 +490,7 @@ public abstract class JavaDslModelWriterSupport {
     }
 
     private void writeChainedErrorHandler(StringBuilder sb, 
ErrorHandlerDefinition errorHandler) {
-        if (errorHandler.getErrorHandlerType() instanceof 
DeadLetterChannelDefinition dlc) {
-            sb.append(NL).append(indent()).append("    
.errorHandler(deadLetterChannel(")
-                    .append(quote(dlc.getDeadLetterUri())).append(")");
-            appendErrorHandlerOptions(sb, dlc);
-            sb.append(")");
-        } else if (errorHandler.getErrorHandlerType() instanceof 
DefaultErrorHandlerDefinition deh) {
-            sb.append(NL).append(indent()).append("    
.errorHandler(defaultErrorHandler()");
-            appendErrorHandlerOptions(sb, deh);
-            sb.append(")");
-        } else {
-            sb.append(NL).append(indent()).append("    
.errorHandler(noErrorHandler())");
-        }
+        sb.append(NL).append(indent()).append("    
.errorHandler(").append(errorHandlerDsl(errorHandler)).append(")");
     }
 
     private void writeTemplateParameter(StringBuilder sb, 
RouteTemplateParameterDefinition param) {
@@ -1225,18 +1222,19 @@ public abstract class JavaDslModelWriterSupport {
         sb.append(";").append(NL).append(NL);
     }
 
-    private void writeErrorHandler(StringBuilder sb, ErrorHandlerDefinition 
errorHandler) {
+    /** deadLetterChannel("uri").maximumRedeliveries(3), defaultErrorHandler() 
or noErrorHandler(). */
+    private String errorHandlerDsl(ErrorHandlerDefinition errorHandler) {
+        StringBuilder sb = new StringBuilder();
         if (errorHandler.getErrorHandlerType() instanceof 
DeadLetterChannelDefinition dlc) {
-            
sb.append("errorHandler(deadLetterChannel(").append(quote(dlc.getDeadLetterUri())).append(")");
+            
sb.append("deadLetterChannel(").append(quote(dlc.getDeadLetterUri())).append(")");
             appendErrorHandlerOptions(sb, dlc);
-            sb.append(");").append(NL).append(NL);
         } else if (errorHandler.getErrorHandlerType() instanceof 
DefaultErrorHandlerDefinition deh) {
-            sb.append("errorHandler(defaultErrorHandler()");
+            sb.append("defaultErrorHandler()");
             appendErrorHandlerOptions(sb, deh);
-            sb.append(");").append(NL).append(NL);
         } else {
-            sb.append("errorHandler(noErrorHandler());").append(NL).append(NL);
+            sb.append("noErrorHandler()");
         }
+        return sb.toString();
     }
 
     private void appendErrorHandlerOptions(StringBuilder sb, 
DefaultErrorHandlerDefinition def) {
@@ -1507,6 +1505,11 @@ public abstract class JavaDslModelWriterSupport {
         if (expr instanceof RefExpression) {
             return "ref(" + quotedValue + ")";
         }
+        String factoryMethod = languageFactoryMethod(expr);
+        if (factoryMethod != null) {
+            // groovy, ognl, ...: as their own expression, not 
language("groovy", ...)
+            return "expression()." + factoryMethod + "(" + quotedValue + 
").end()";
+        }
         String lang = expr.getLanguage();
         if (lang != null && !lang.isEmpty()) {
             return "language(" + quote(lang) + ", " + quotedValue + ")";
@@ -1557,6 +1560,10 @@ public abstract class JavaDslModelWriterSupport {
         if (expr instanceof WasmExpression) {
             return "wasm";
         }
+        String factoryMethod = languageFactoryMethod(expr);
+        if (factoryMethod != null) {
+            return factoryMethod;
+        }
         String lang = expr.getLanguage();
         if (lang != null && !lang.isEmpty()) {
             return "language";
@@ -1564,6 +1571,23 @@ public abstract class JavaDslModelWriterSupport {
         return null;
     }
 
+    /**
+     * The method of expression() that builds this kind of expression from its 
text (groovy("..."), ognl("...")), or
+     * null when there is none.
+     */
+    private static String languageFactoryMethod(ExpressionDefinition expr) {
+        String lang = expr.getLanguage();
+        if (lang == null || lang.isEmpty() || expr instanceof 
LanguageExpression) {
+            return null;
+        }
+        try {
+            Method m = LanguageBuilderFactory.class.getMethod(lang, 
String.class);
+            return m.getReturnType().getEnclosingClass() == expr.getClass() ? 
lang : null;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
     private String expressionBuilderOptions(ExpressionDefinition expr) {
         StringBuilder opts = new StringBuilder();
 
@@ -1576,6 +1600,15 @@ public abstract class JavaDslModelWriterSupport {
             appendOption(opts, "source", single.getSource());
         }
 
+        // the xml namespaces of xpath, xquery and xtokenize (those of the xml 
document the route was read from)
+        if (expr instanceof NamespaceAwareExpression nae && 
nae.getNamespaces() != null
+                && !nae.getNamespaces().isEmpty()) {
+            opts.append(".namespaces(Map.of(");
+            StringJoiner pairs = new StringJoiner(", ");
+            nae.getNamespaces().forEach((prefix, uri) -> 
pairs.add(quote(prefix) + ", " + quote(uri)));
+            opts.append(pairs).append("))");
+        }
+
         // type-specific options
         if (expr instanceof SimpleExpression se) {
             appendNonDefaultOption(opts, "trimResult", se.getTrimResult(), 
"false");
diff --git 
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/RoundTripTest.java 
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/RoundTripTest.java
index e05dbad37745..459de6a366f4 100644
--- 
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/RoundTripTest.java
+++ 
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/RoundTripTest.java
@@ -42,6 +42,7 @@ import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.java.LwModelToJavaDumper;
 import org.apache.camel.model.RouteDefinition;
 import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.xml.LwModelToXMLDumper;
 import org.apache.camel.xml.in.ModelParser;
 import org.junit.jupiter.api.Test;
 
@@ -130,6 +131,58 @@ class RoundTripTest {
                 String.join("\n\n", failures)).isEmpty();
     }
 
+    /**
+     * Routes whose Java reads back to a model that differs from the XML one, 
and why: none of them loses what the route
+     * does.
+     */
+    private static final Map<String, String> KNOWN_XML = Map.of(
+            "barInterceptorRoute.xml", "Java has intercept() for all the 
routes of a route builder, not of one route",
+            "interceptFrom.xml", "Java has interceptFrom() for all the routes 
of a route builder, not of one route",
+            "interceptFromAndSendTo.xml",
+            "Java has interceptSendToEndpoint() for all the routes of a route 
builder, not of one route",
+            "routeInlinedErrorHandler.xml",
+            "the redelivery policy of deadLetterChannel() in Java says 
logExhausted=false, the default of a dead letter channel",
+            "circuitBreakerResilience4j.xml", "failureRateThreshold(float) 
keeps 30 as 30.0: the same value, written apart");
+
+    /**
+     * The routes of the XML corpus read back from the Java the dumper writes 
as the XML routes they were (CAMEL-25255):
+     * the Java writer leaves nothing of the route out, such as its error 
handler or the namespaces of an xpath.
+     */
+    @Test
+    void xmlCorpusReadsBackThroughJava() throws Exception {
+        assumeTrue(Files.isDirectory(CORPUS.get(0)), "the core test routes are 
next to this module");
+        List<Path> files = new ArrayList<>();
+        for (Path dir : CORPUS) {
+            try (Stream<Path> s = Files.list(dir)) {
+                files.addAll(s.filter(p -> 
p.toString().endsWith(".xml")).sorted().toList());
+            }
+        }
+        List<String> failures = new ArrayList<>();
+        int routes = 0;
+        for (Path file : files) {
+            if (KNOWN_XML.containsKey(file.getFileName().toString())) {
+                continue;
+            }
+            for (RouteDefinition route : routes(file)) {
+                routes++;
+                try (DefaultCamelContext context = new DefaultCamelContext()) {
+                    String xml = new 
LwModelToXMLDumper().dumpModelAsXml(context, route);
+                    String java = new 
LwModelToJavaDumper().dumpModelAsJava(context, route);
+                    JavaParseResult parsed = new LwJavaParser().parse(java + 
";");
+                    String back = parsed.routes().getRoutes().isEmpty()
+                            ? "" : new 
LwModelToXMLDumper().dumpModelAsXml(context, 
parsed.routes().getRoutes().get(0));
+                    if (!back.equals(xml)) {
+                        failures.add(file.getFileName() + "\n" + java + "\n--- 
was ---\n" + xml + "\n--- read back as ---\n"
+                                     + back);
+                    }
+                }
+            }
+        }
+        assertThat(routes).isGreaterThan(100);
+        assertThat(failures).as("%d of %d routes do not read back:%n%s", 
failures.size(), routes,
+                String.join("\n\n", failures)).isEmpty();
+    }
+
     /** The Java the dumper is tested to write (java-dsl-*.txt) reads back 
into the same Java. */
     @Test
     void dumperGoldenFiles() throws Exception {
diff --git 
a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java
 
b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java
index edb798a0291f..af32449f2765 100644
--- 
a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java
+++ 
b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java
@@ -444,6 +444,7 @@ public class JavaDslCompileTest {
     private static String wrapInRouteBuilder(String className, List<String> 
routeSnippets) {
         StringBuilder sb = new StringBuilder();
         sb.append("package test.generated;\n\n");
+        sb.append("import java.util.Map;\n\n");
         sb.append("import org.apache.camel.ExchangePattern;\n");
         sb.append("import org.apache.camel.LoggingLevel;\n");
         sb.append("import org.apache.camel.Predicate;\n");
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 2f36e4a00e02..6978202d6eb8 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
@@ -4184,3 +4184,23 @@ WebSocket transport already were. Before, it kept the 
JVM alive when the applica
 Spring Boot application whose port was in use: Camel had not started, so it 
never stopped the connector. An application
 that only kept running because of this thread now exits; keep it running on 
purpose, for example with
 `camel.main.run-controller=true` for a Spring Boot application without a web 
server.
+
+=== Route dumps as YAML and Java
+
+Routes dumped as YAML (`camel.main.dumpRoutes=yaml`, `camel transform route 
--format=yaml`) are now written as the
+YAML DSL loads them:
+
+* the verbs of a rest are listed by their kind (`get`, `post`, ...) instead of 
under `verbs`, and their parameters and
+  response messages as `param` and `responseMessage`;
+* a route template has `parameters` and `beans` instead of `templateParameter` 
and `templateBean`;
+* the properties of a bean are a map, also when they are nested, instead of a 
`property` list.
+
+Before, such a dump did not load in the YAML DSL.
+
+A route built in Java with its own error handler 
(`from("...").errorHandler(deadLetterChannel("..."))`) now has the
+error handler in its model, so dumping the route (as XML, YAML or Java) writes 
it; it was left out before. When written
+as Java, the error handler of an XML or YAML route is now `.errorHandler(...)` 
on the route, instead of an
+`errorHandler(...)` that applies to every route of the route builder.
+
+The `timeout(long)` of the Resequencer EIP in Java keeps the millis as given 
(`2000`), instead of a duration
+(`2s0ms`).

Reply via email to