You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/02 06:59:52 UTC
(camel) 02/02: CAMEL-25255: camel-java-io - the Java writer keeps the route error handler, xpath namespaces and the kind of language
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 <cl...@gmail.com>
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) <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../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`).