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`).
