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 a00b817e6846 CAMEL-25254: camel-jbang - convert a route file between 
YAML, XML and Java DSL without running it (#27251)
a00b817e6846 is described below

commit a00b817e68465d5b7de952718f2f428582f9480a
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 09:28:26 2026 +0200

    CAMEL-25254: camel-jbang - convert a route file between YAML, XML and Java 
DSL without running it (#27251)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/images/jbang/camel-tui-source-convert.png | Bin 0 -> 253954 bytes
 .../ROOT/pages/camel-jbang-transforming.adoc       |  19 +-
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |  10 +
 .../camel-jbang-transform-route.adoc               |   4 +-
 .../jbang-commands/camel-jbang-transform.adoc      |   2 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../dsl/jbang/core/commands/RouteDslConverter.java | 567 +++++++++++++++++++++
 .../dsl/jbang/core/commands/TransformRoute.java    |  49 +-
 .../jbang/core/commands/RouteDslConverterTest.java | 218 ++++++++
 .../jbang/core/commands/tui/FileActionsPopup.java  |  32 +-
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  33 +-
 .../dsl/jbang/core/commands/tui/TuiIcons.java      |   1 +
 .../src/main/resources/tui/help/source.md          |   4 +-
 .../jbang/core/commands/tui/SourceConvertTest.java |  73 +++
 14 files changed, 1000 insertions(+), 14 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-convert.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-convert.png
new file mode 100644
index 000000000000..5c9d2d4ef81d
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-convert.png differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-transforming.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-transforming.adoc
index c136c90d73d8..3e58e7de2e77 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-transforming.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-transforming.adoc
@@ -427,7 +427,7 @@ Use `--predicate` to evaluate as a boolean predicate 
instead of a value expressi
 
 == Transforming route DSL format
 
-The `camel transform route` command converts between YAML and XML DSL formats —
+The `camel transform route` command converts between the YAML, XML and Java 
DSL formats —
 useful when working with graphical editors or code generators that target a 
specific DSL.
 
 For example, given this YAML route (`route.yaml`):
@@ -512,3 +512,20 @@ written as a running Camel would dump them. This takes a 
fraction of a second, a
 When a route cannot be read completely that way, for example a processor 
written as a lambda or a value only known
 at runtime, the routes are compiled and started to dump them, which takes a 
few seconds and may download dependencies.
 The same happens with `--resolve-placeholders`. Use `--compile` to always 
compile and start the routes.
+
+=== Transforming to Java
+
+YAML and XML routes are transformed to the Java DSL with `--format=java`, one 
route builder class per file, named
+after it (`orders.camel.yaml` becomes `Orders.java`):
+
+[source,bash]
+----
+camel transform route --format=java orders.camel.yaml
+----
+
+Nothing runs: the file is read into the model without running it (the beans of 
a YAML file are read as definitions
+and not created), written as Java, and read back to check the Java has the 
same routes. What does not carry over is
+printed as a note: the comments, the beans (which Java declares in code, such 
as with `@BindToRegistry`), and anything
+the Java DSL writer does not write, named by the first difference found. A 
file that cannot be converted without
+losing part of it is refused with the reason, for example a Java route with a 
processor written as a lambda, or a
+predicate built in Java (`header("x").isEqualTo("y")`) which YAML and XML 
cannot express.
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index 6996a256a66a..8fc4b368a97c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -263,6 +263,16 @@ image::jbang/camel-tui-source-format-cycling.png[A Java 
route shown in YAML]
 This is handy to see how a route reads in another DSL, or to copy it into a 
file of another DSL. Press *p* for plain
 mode, which hides the line numbers and borders for easy copying.
 
+To convert a whole file, select it in the file list, press *F12* and pick 
_Convert to YAML_, _Convert to XML_ or
+_Convert to Java_. This works on files at rest, without running them: the file 
is read into the model (the beans of a
+YAML file as definitions, not created), written in the other DSL into a new 
file next to it (`orders.camel.xml` becomes
+`orders.camel.yaml`, or the route builder class `Orders.java`), and read back 
to check it has the same routes. The new
+file opens; the original stays as it is, and an existing file is not 
overwritten. What does not carry over is said at
+the top of the new file, such as the comments or a part the other DSL's writer 
leaves out, and a file that would lose
+something it cannot say (a lambda processor, a predicate built in Java code) 
is not converted, with the reason why.
+
+image::jbang/camel-tui-source-convert.png[An XML route converted to YAML, with 
the note of what did not carry over]
+
 == Keyboard Shortcuts
 
 === Viewing
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-route.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-route.adoc
index ff61e758b31c..1a5761578524 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-route.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-route.adoc
@@ -2,7 +2,7 @@
 // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
 = camel transform route
 
-Transform Camel routes to XML or YAML format
+Transform Camel routes to XML, YAML or Java format
 
 
 == Usage
@@ -21,7 +21,7 @@ camel transform route [options]
 | Option | Description | Default | Type
 | `--compile` | Compile and run Java routes to transform them. By default Java 
routes are read without compiling them, and compiled only when a route cannot 
be read that way | false | boolean
 | `--download` | Whether to allow automatic downloading JAR dependencies (over 
the internet) | true | boolean
-| `--format` | Output format (xml, yaml), if only yaml files are provided, the 
format defaults to xml and vice versa |  | String
+| `--format` | Output format (xml, yaml, java), if only yaml files are 
provided, the format defaults to xml and vice versa. Java is converted without 
running the routes, one route builder class per file |  | String
 | `--fresh` | Make sure we use fresh (i.e. non-cached) resources | false | 
boolean
 | `--ignore-loading-error` | Whether to ignore route loading and compilation 
errors (use this with care!) |  | boolean
 | `--output` | File or directory to store transformed files. If none provide 
then output is printed to console. Use clipboard as name to copy content into 
clipboard. |  | String
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform.adoc
index 79c46f128eea..7aedee1719e0 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform.adoc
@@ -20,7 +20,7 @@ camel transform [options]
 | Subcommand | Description
 | xref:jbang-commands/camel-jbang-transform-dataweave.adoc[dataweave] | 
Convert DataWeave scripts to DataSonnet format
 | xref:jbang-commands/camel-jbang-transform-message.adoc[message] | Transform 
message from one format to another via an existing running Camel integration
-| xref:jbang-commands/camel-jbang-transform-route.adoc[route] | Transform 
Camel routes to XML or YAML format
+| xref:jbang-commands/camel-jbang-transform-route.adoc[route] | Transform 
Camel routes to XML, YAML or Java format
 |===
 
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 3fe53a77e6ec..d4507d17043b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -34,7 +34,7 @@
     { "name": "stop", "fullName": "stop", "description": "Shuts down running 
Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ { 
"names": "--kill", "description": "To force killing the process (SIGKILL)", 
"javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", 
"description": "Display the help and sub-commands", "javaType": "boolean", 
"type": "boolean" } ] },
     { "name": "top", "fullName": "top", "description": "Top status of Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ { 
"names": "--watch", "description": "Execute periodically and showing output 
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context", 
"fullName": "top  [...]
     { "name": "trace", "fullName": "trace", "description": "Tail message 
traces from running Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction", "options": 
[ { "names": "--action", "description": "Action to start, stop, clear, list 
status, or dump traces", "defaultValue": "status", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--ago", "description": "Use 
ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "b [...]
-    { "name": "transform", "fullName": "transform", "description": "Transform 
message or Camel routes", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"dataweave", "fullName": "transform dataweave", "description": "Convert 
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl. 
[...]
+    { "name": "transform", "fullName": "transform", "description": "Transform 
message or Camel routes", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"dataweave", "fullName": "transform dataweave", "description": "Convert 
DataWeave scripts to DataSonnet format", "sourceClass": "org.apache.camel.dsl. 
[...]
     { "name": "update", "fullName": "update", "description": "Update Camel 
project", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"list", "fullName": "update list", "description": "List available update 
versions for Camel and its runtime variants", "sourceClass": 
"org.apache.camel.dsl.jbang.cor [...]
     { "name": "version", "fullName": "version", "description": "Manage Camel 
versions", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.version.VersionCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "version get", "description": "Displays current Camel version", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.version.VersionGet", 
[...]
     { "name": "wrapper", "fullName": "wrapper", "description": "Install Camel 
wrapper scripts for version pinning", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.WrapperCommand", "options": [ { 
"names": "--camel-version", "description": "Camel version to pin (defaults to 
current version)", "javaType": "java.lang.String", "type": "string" }, { 
"names": "--command-name", "description": "The name of the command to use 
(camel or camelw)", "defaultValue": "camel", "javaType": "java [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
new file mode 100644
index 000000000000..b5fa941e030a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverter.java
@@ -0,0 +1,567 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.DefaultDumpRoutesStrategy;
+import org.apache.camel.java.in.JavaParseResult;
+import org.apache.camel.model.BeanFactoryDefinition;
+import org.apache.camel.model.Model;
+import org.apache.camel.model.RouteConfigurationDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.RouteTemplateDefinition;
+import org.apache.camel.model.TemplatedRouteDefinition;
+import org.apache.camel.model.app.BeansDefinition;
+import org.apache.camel.model.rest.RestDefinition;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.xml.in.ModelParser;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+
+/**
+ * Converts a route file from one DSL to another (YAML, XML, Java) without 
running it (CAMEL-25254): the file is read
+ * into the model of a Camel context that is never started, and written with 
the model writers. No code of the file or
+ * the project runs and no class of it is loaded: Java is read by the Java DSL 
parser, XML by the XML parser, YAML by
+ * the YAML DSL with its beans read as definitions only (the YAML DSL would 
create them). Comments do not survive, and a
+ * Java route the parser cannot read completely (a lambda, a processor) is 
refused rather than converted with a gap.
+ */
+public final class RouteDslConverter {
+
+    /** The DSLs a file is converted between. */
+    public static final List<String> FORMATS = List.of("yaml", "xml", "java");
+
+    /**
+     * The result of a conversion.
+     *
+     * @param content  the file in the target DSL, null when refused
+     * @param fileName the name of the converted file next to the original 
(orders.camel.yaml, Orders.java)
+     * @param notes    what the converted file does not carry over 
(comments...)
+     * @param refused  why the file is not converted, null when it is
+     */
+    public record Result(String content, String fileName, List<String> notes, 
String refused) {
+
+        public boolean converted() {
+            return refused == null;
+        }
+
+        static Result refuse(String why) {
+            return new Result(null, null, List.of(), why);
+        }
+    }
+
+    private static final Pattern YAML_BEANS = 
Pattern.compile("^-\\s+beans\\s*:");
+    private static final Pattern COMMENT = 
Pattern.compile("(?m)^\\s*(#|//|/\\*|<!--)");
+
+    private RouteDslConverter() {
+    }
+
+    /** The converted file with what did not carry over as a comment at its 
top, in the comment syntax of the DSL. */
+    public static String withNotes(String content, List<String> notes, String 
format) {
+        if (notes.isEmpty()) {
+            return content;
+        }
+        StringBuilder sb = new StringBuilder();
+        switch (format) {
+            case "xml" -> {
+                sb.append("<!--\n");
+                notes.forEach(n -> sb.append("  ").append(n.replace("--", "- 
-")).append('\n'));
+                sb.append("-->\n");
+            }
+            case "java" -> notes.forEach(n -> sb.append("// 
").append(n).append('\n'));
+            default -> notes.forEach(n -> sb.append("# 
").append(n).append('\n'));
+        }
+        return sb.append(content).toString();
+    }
+
+    /** The DSL of a file by its name: yaml, xml or java; null for another 
file. */
+    public static String formatOf(String fileName) {
+        String name = fileName.toLowerCase(Locale.ROOT);
+        if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+            return "yaml";
+        }
+        if (name.endsWith(".xml")) {
+            return "xml";
+        }
+        if (name.endsWith(".java")) {
+            return "java";
+        }
+        return null;
+    }
+
+    /** Converts the file to the format, with the Java sources of its folder 
for the constants a Java route uses. */
+    public static Result convert(Path file, String format) throws IOException {
+        String content = Files.readString(file, StandardCharsets.UTF_8);
+        return convert(file.getFileName().toString(), content, format, 
javaSources(file));
+    }
+
+    /** Converts the content of a file of the given name to the format. */
+    public static Result convert(
+            String fileName, String content, String format, Map<String, 
Supplier<String>> javaSources) {
+        String from = formatOf(fileName);
+        if (from == null) {
+            return Result.refuse(fileName + " is no YAML, XML or Java route 
file");
+        }
+        if (!FORMATS.contains(format)) {
+            return Result.refuse("Unknown format " + format + ": one of " + 
FORMATS);
+        }
+        if (from.equals(format)) {
+            return Result.refuse(fileName + " is " + format + " already");
+        }
+        Result result = read(fileName, content, from, format, javaSources);
+        if (!result.converted()) {
+            return result;
+        }
+        return verify(fileName, content, from, format, javaSources, result);
+    }
+
+    /**
+     * Reads the converted file back and compares its model with the 
original's, both written as XML: a file that does
+     * not load is refused (a writer of the target DSL lacks something), a 
difference (an error handler the target DSL
+     * does not write) is said in the notes, so nothing is lost without a word.
+     */
+    private static Result verify(
+            String fileName, String content, String from, String format, 
Map<String, Supplier<String>> javaSources,
+            Result result) {
+        Result back = read(result.fileName(), result.content(), format, "xml", 
javaSources);
+        if (!back.converted()) {
+            return Result.refuse("the converted " + 
format.toUpperCase(Locale.ROOT) + " would not load ("
+                                 + back.refused() + "): " + fileName + " 
cannot be converted to it yet");
+        }
+        Result original = read(fileName, content, from, "xml", javaSources);
+        if (!original.converted()) {
+            return result;
+        }
+        String expected = original.content();
+        if ("java".equals(format)) {
+            // the beans do not go to Java, which the notes say already
+            expected = withoutBeans(expected);
+        }
+        String difference = firstDifference(expected, back.content());
+        if (difference == null) {
+            return result;
+        }
+        List<String> notes = new ArrayList<>(result.notes());
+        notes.add("Check the converted file, its routes differ from " + 
fileName + ": " + difference);
+        return new Result(result.content(), result.fileName(), notes, null);
+    }
+
+    /** An XML dump without its bean definitions, nested properties included; 
the bean EIPs of routes stay. */
+    static String withoutBeans(String xml) {
+        StringBuilder sb = new StringBuilder();
+        boolean inBean = false;
+        for (String line : xml.split("\n")) {
+            String t = line.strip();
+            // a bean definition (name, type), not the bean EIP of a route 
(ref, beanType)
+            if (!inBean && t.startsWith("<bean ") && !t.contains(" ref=")
+                    && (t.contains(" name=") || t.contains(" type="))) {
+                inBean = !t.endsWith("/>");
+                continue;
+            }
+            if (inBean) {
+                inBean = !t.equals("</bean>");
+                continue;
+            }
+            sb.append(line).append('\n');
+        }
+        return sb.toString();
+    }
+
+    /** The first line that differs between two dumps, as expected / found; 
null when they are the same. */
+    static String firstDifference(String expected, String found) {
+        List<String> a = significant(expected);
+        List<String> b = significant(found);
+        for (int i = 0; i < Math.max(a.size(), b.size()); i++) {
+            String x = i < a.size() ? a.get(i) : "(nothing)";
+            String y = i < b.size() ? b.get(i) : "(nothing)";
+            if (!x.equals(y)) {
+                return "expected " + x + " but found " + y;
+            }
+        }
+        return null;
+    }
+
+    private static List<String> significant(String dump) {
+        List<String> lines = new ArrayList<>();
+        for (String line : dump.split("\n")) {
+            String t = line.strip();
+            if (!t.isEmpty()) {
+                lines.add(t);
+            }
+        }
+        return lines;
+    }
+
+    /** The model of the file written in the format, also its own (tests 
compare the models of two files this way). */
+    static Result read(
+            String fileName, String content, String from, String format, 
Map<String, Supplier<String>> javaSources) {
+        Resource resource = ResourceHelper.fromString("file:" + fileName, 
content);
+        List<String> notes = new ArrayList<>();
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            // never started: adding the definitions only registers them
+            String why = switch (from) {
+                case "java" -> loadJava(context, fileName, content, 
javaSources, resource, notes);
+                case "xml" -> loadXml(context, content, resource);
+                default -> loadYaml(context, content, resource);
+            };
+            if (why != null) {
+                return Result.refuse(why);
+            }
+            Model model = 
context.getCamelContextExtension().getContextPlugin(Model.class);
+            if (model.getRouteDefinitions().isEmpty() && 
model.getRestDefinitions().isEmpty()
+                    && model.getRouteTemplateDefinitions().isEmpty() && 
model.getRouteConfigurationDefinitions().isEmpty()
+                    && model.getCustomBeans().isEmpty()) {
+                return Result.refuse(fileName + " has no routes, rests, route 
configurations, templates or beans");
+            }
+            if ("java".equals(format) && model.getRouteDefinitions().isEmpty() 
&& model.getRestDefinitions().isEmpty()
+                    && model.getRouteTemplateDefinitions().isEmpty()) {
+                return Result.refuse(fileName + " has only beans, which Java 
declares in code (such as with"
+                                     + " @BindToRegistry)");
+            }
+            if ("java".equals(format) && 
!model.getRouteConfigurationDefinitions().isEmpty()) {
+                return Result.refuse("route configurations are written in Java 
as a RouteConfigurationBuilder;"
+                                     + " convert " + fileName + " without 
them");
+            }
+            String output = dump(context, format);
+            if (!"java".equals(format) && 
output.contains("expressionDefinition")) {
+                // a predicate or expression built in Java code has no YAML or 
XML form: the file would not load
+                return Result.refuse(fileName + " uses a predicate or 
expression built in Java (such as"
+                                     + " header(\"x\").isEqualTo(..)), which 
has no " + format.toUpperCase(Locale.ROOT)
+                                     + " form: write it with simple first");
+            }
+            if (COMMENT.matcher(content).find()) {
+                notes.add("The comments of " + fileName + " are not carried 
over");
+            }
+            String target = targetName(fileName, format);
+            if ("java".equals(format)) {
+                if (!model.getCustomBeans().isEmpty()) {
+                    notes.add("The beans of " + fileName
+                              + " are not carried over: declare them in Java, 
such as with @BindToRegistry");
+                }
+                output = javaClass(target.substring(0, target.length() - 
".java".length()), output);
+            }
+            return new Result(output, target, notes, null);
+        } catch (Exception e) {
+            return Result.refuse(fileName + " could not be read: " + 
e.getMessage());
+        }
+    }
+
+    /** The types a dumped Java route may use beside the route builder's own 
methods, imported when it does. */
+    private static final Map<String, String> JAVA_TYPES = Map.of(
+            "Map", "java.util.Map",
+            "LoggingLevel", "org.apache.camel.LoggingLevel",
+            "ExchangePattern", "org.apache.camel.ExchangePattern",
+            "WaitForTaskToComplete", "org.apache.camel.WaitForTaskToComplete",
+            "RestBindingMode", "org.apache.camel.model.rest.RestBindingMode",
+            "RestParamType", "org.apache.camel.model.rest.RestParamType",
+            "CollectionFormat", "org.apache.camel.model.rest.CollectionFormat",
+            "ShutdownRoute", "org.apache.camel.ShutdownRoute",
+            "ShutdownRunningTask", "org.apache.camel.ShutdownRunningTask");
+
+    /** The dumped routes as the configure() of a route builder class, with 
the imports they need. */
+    static String javaClass(String className, String routes) {
+        StringBuilder sb = new StringBuilder();
+        List<String> imports = new ArrayList<>();
+        JAVA_TYPES.forEach((simple, fqn) -> {
+            if (Pattern.compile("\\b" + simple + 
"\\.").matcher(routes).find()) {
+                imports.add(fqn);
+            }
+        });
+        imports.add("org.apache.camel.builder.RouteBuilder");
+        imports.sort(null);
+        imports.forEach(i -> sb.append("import ").append(i).append(";\n"));
+        sb.append("\npublic class ").append(className).append(" extends 
RouteBuilder {\n\n");
+        sb.append("    @Override\n    public void configure() throws Exception 
{\n");
+        for (String line : routes.strip().split("\n")) {
+            sb.append(line.isBlank() ? "" : "        " + line).append('\n');
+        }
+        sb.append("    }\n}\n");
+        return sb.toString();
+    }
+
+    /** The name of the converted file: the same base name, as Camel names 
route files of the DSL. */
+    static String targetName(String fileName, String format) {
+        String base = fileName;
+        for (String ext : List.of(".camel.yaml", ".camel.yml", ".camel.xml", 
".yaml", ".yml", ".xml", ".java")) {
+            if (base.toLowerCase(Locale.ROOT).endsWith(ext)) {
+                base = base.substring(0, base.length() - ext.length());
+                break;
+            }
+        }
+        if ("java".equals(format)) {
+            return javaClassName(base) + ".java";
+        }
+        return base + ".camel." + format;
+    }
+
+    /** orders-route to OrdersRoute: a Java class name of a base name. */
+    static String javaClassName(String base) {
+        StringBuilder sb = new StringBuilder();
+        boolean upper = true;
+        for (char c : base.toCharArray()) {
+            if (Character.isLetterOrDigit(c)) {
+                sb.append(upper ? Character.toUpperCase(c) : c);
+                upper = false;
+            } else {
+                upper = true;
+            }
+        }
+        if (sb.isEmpty() || !Character.isJavaIdentifierStart(sb.charAt(0))) {
+            sb.insert(0, "Route");
+        }
+        return sb.toString();
+    }
+
+    private static String loadJava(
+            DefaultCamelContext context, String fileName, String content, 
Map<String, Supplier<String>> javaSources,
+            Resource resource, List<String> notes)
+            throws Exception {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        JavaParseResult r = ProjectRoutes.parseJava(content, javaSources, 
catalog);
+        for (JavaParseResult.Unresolved u : r.unresolved()) {
+            if (!JavaParseResult.configuresTheContext(u)) {
+                return fileName + ":" + u.line() + " cannot be converted 
without running it: " + u.text() + " ("
+                       + u.reason() + ")";
+            }
+            // a statement configuring the Camel context (semantic 
declarations, context settings) is no route: said
+            notes.add("Not carried over, it configures the Camel context 
rather than the routes: " + u.text() + " ("
+                      + fileName + ":" + u.line() + ")");
+        }
+        r.routes().getRoutes().forEach(d -> d.setResource(resource));
+        r.rests().getRests().forEach(d -> d.setResource(resource));
+        r.routeTemplates().getRouteTemplates().forEach(d -> 
d.setResource(resource));
+        r.routeConfigurations().getRouteConfigurations().forEach(d -> 
d.setResource(resource));
+        
context.addRouteConfigurations(r.routeConfigurations().getRouteConfigurations());
+        context.addRestDefinitions(r.rests().getRests(), false);
+        
context.addRouteTemplateDefinitions(r.routeTemplates().getRouteTemplates());
+        context.addRouteDefinitions(r.routes().getRoutes());
+        return null;
+    }
+
+    private static String loadXml(DefaultCamelContext context, String content, 
Resource resource) throws Exception {
+        String root = xmlRoot(content);
+        if (root == null) {
+            return "no XML root element";
+        }
+        String namespace = xmlNamespace(content);
+        ModelParser parser = new ModelParser(new StringReader(content), 
namespace);
+        List<RouteDefinition> routes = new ArrayList<>();
+        List<RestDefinition> rests = new ArrayList<>();
+        List<RouteConfigurationDefinition> configurations = new ArrayList<>();
+        List<RouteTemplateDefinition> templates = new ArrayList<>();
+        List<TemplatedRouteDefinition> templated = new ArrayList<>();
+        List<BeanFactoryDefinition> beans = new ArrayList<>();
+        switch (root) {
+            case "camel", "beans" -> {
+                BeansDefinition b = parser.parseBeansDefinition().orElse(null);
+                if (b != null) {
+                    routes.addAll(b.getRoutes());
+                    rests.addAll(b.getRests());
+                    configurations.addAll(b.getRouteConfigurations());
+                    templates.addAll(b.getRouteTemplates());
+                    templated.addAll(b.getTemplatedRoutes());
+                    beans.addAll(b.getBeans());
+                }
+            }
+            case "routes", "route" -> 
parser.parseRoutesDefinition().ifPresent(r -> routes.addAll(r.getRoutes()));
+            case "rests", "rest" -> parser.parseRestsDefinition().ifPresent(r 
-> rests.addAll(r.getRests()));
+            case "routeConfigurations", "routeConfiguration" -> 
parser.parseRouteConfigurationsDefinition()
+                    .ifPresent(c -> 
configurations.addAll(c.getRouteConfigurations()));
+            case "routeTemplates", "routeTemplate" -> 
parser.parseRouteTemplatesDefinition()
+                    .ifPresent(t -> templates.addAll(t.getRouteTemplates()));
+            case "templatedRoutes", "templatedRoute" -> 
parser.parseTemplatedRoutesDefinition()
+                    .ifPresent(t -> templated.addAll(t.getTemplatedRoutes()));
+            default -> {
+                return "<" + root + "> is no Camel XML DSL root";
+            }
+        }
+        routes.forEach(d -> d.setResource(resource));
+        rests.forEach(d -> d.setResource(resource));
+        configurations.forEach(d -> d.setResource(resource));
+        templates.forEach(d -> d.setResource(resource));
+        context.addRouteConfigurations(configurations);
+        context.addRestDefinitions(rests, false);
+        context.addRouteTemplateDefinitions(templates);
+        context.addRouteDefinitions(routes);
+        Model model = 
context.getCamelContextExtension().getContextPlugin(Model.class);
+        for (BeanFactoryDefinition bean : beans) {
+            bean.setResource(resource);
+            model.addCustomBean(bean);
+        }
+        if (!templated.isEmpty()) {
+            return "templated routes cannot be converted yet";
+        }
+        return null;
+    }
+
+    private static final Pattern YAML_SEMANTIC = 
Pattern.compile("(?m)^-\\s+semantic\\s*:");
+
+    private static String loadYaml(DefaultCamelContext context, String 
content, Resource resource) throws Exception {
+        if (YAML_SEMANTIC.matcher(content).find()) {
+            // they live outside the route model the writers export: 
converting would lose them without a word
+            return "semantic declarations cannot be converted with the routes: 
keep them in a separate declaration"
+                   + " resource and convert only the routes";
+        }
+        // the beans are taken out and read as definitions: the YAML DSL would 
create them, running code of the project
+        StringBuilder routes = new StringBuilder();
+        StringBuilder beans = new StringBuilder();
+        boolean inBeans = false;
+        for (String line : content.split("\n", -1)) {
+            if (line.startsWith("-")) {
+                inBeans = YAML_BEANS.matcher(line).find();
+            }
+            (inBeans ? beans : routes).append(line).append('\n');
+        }
+        if (!routes.toString().isBlank()) {
+            Resource r = ResourceHelper.fromString(resource.getLocation(), 
routes.toString());
+            PluginHelper.getRoutesLoader(context).loadRoutes(r);
+        }
+        if (!beans.isEmpty()) {
+            Model model = 
context.getCamelContextExtension().getContextPlugin(Model.class);
+            for (BeanFactoryDefinition bean : yamlBeans(beans.toString())) {
+                bean.setResource(resource);
+                model.addCustomBean(bean);
+            }
+        }
+        return null;
+    }
+
+    /** The beans of the YAML beans entries as definitions, read as plain 
data: no class is loaded or created. */
+    @SuppressWarnings("unchecked")
+    static List<BeanFactoryDefinition> yamlBeans(String yaml) {
+        List<BeanFactoryDefinition> found = new ArrayList<>();
+        Object root = new Yaml(new SafeConstructor(new 
LoaderOptions())).load(yaml);
+        if (!(root instanceof List<?> entries)) {
+            return found;
+        }
+        for (Object entry : entries) {
+            if (entry instanceof Map<?, ?> m && m.get("beans") instanceof 
List<?> list) {
+                for (Object o : list) {
+                    if (o instanceof Map<?, ?> b) {
+                        found.add(bean((Map<String, Object>) b));
+                    }
+                }
+            }
+        }
+        return found;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private static BeanFactoryDefinition bean(Map<String, Object> b) {
+        BeanFactoryDefinition def = new BeanFactoryDefinition();
+        def.setName(string(b.get("name")));
+        def.setType(string(b.get("type")));
+        def.setInitMethod(string(b.get("initMethod")));
+        def.setDestroyMethod(string(b.get("destroyMethod")));
+        def.setFactoryMethod(string(b.get("factoryMethod")));
+        def.setFactoryBean(string(b.get("factoryBean")));
+        def.setBuilderClass(string(b.get("builderClass")));
+        def.setBuilderMethod(string(b.get("builderMethod")));
+        def.setScriptLanguage(string(b.get("scriptLanguage")));
+        def.setScript(string(b.get("script")));
+        if (b.get("properties") instanceof Map<?, ?> p) {
+            def.setProperties(new LinkedHashMap<>((Map<String, Object>) p));
+        }
+        if (b.get("constructors") instanceof Map<?, ?> c) {
+            Map<Integer, Object> constructors = new LinkedHashMap<>();
+            ((Map) c).forEach((k, v) -> 
constructors.put(Integer.valueOf(String.valueOf(k)), v));
+            def.setConstructors(constructors);
+        }
+        return def;
+    }
+
+    private static String string(Object o) {
+        return o != null ? String.valueOf(o) : null;
+    }
+
+    /** The model of the context written in the format, as the route dump of a 
running application writes it. */
+    private static String dump(DefaultCamelContext context, String format) 
throws IOException {
+        Path dir = Files.createTempDirectory("camel-convert");
+        try {
+            Path target = dir.resolve("converted." + format);
+            DefaultDumpRoutesStrategy dumper = new DefaultDumpRoutesStrategy();
+            dumper.setCamelContext(context);
+            
dumper.setInclude("routes,rests,routeConfigurations,routeTemplates,beans,dataFormats");
+            dumper.setLog(false);
+            dumper.setResolvePlaceholders(false);
+            dumper.setUriAsParameters(false);
+            dumper.setOutput(target.toString());
+            dumper.dumpRoutes(format);
+            return Files.isRegularFile(target) ? Files.readString(target, 
StandardCharsets.UTF_8) : "";
+        } finally {
+            try (Stream<Path> walk = Files.walk(dir)) {
+                walk.sorted(Comparator.reverseOrder()).forEach(p -> 
p.toFile().delete());
+            }
+        }
+    }
+
+    private static final Pattern XML_ROOT = 
Pattern.compile("<([A-Za-z_][\\w.-]*:)?([A-Za-z_][\\w.-]*)[\\s>/]");
+
+    /** The local name of the root element of an XML document, past its prolog 
and comments. */
+    static String xmlRoot(String content) {
+        Matcher m = 
XML_ROOT.matcher(content.replaceAll("(?s)<\\?.*?\\?>|<!--.*?-->|<!DOCTYPE[^>]*>",
 ""));
+        return m.find() ? m.group(2) : null;
+    }
+
+    private static final Pattern XMLNS = 
Pattern.compile("\\sxmlns=\"([^\"]*)\"");
+
+    private static String xmlNamespace(String content) {
+        Matcher m = XMLNS.matcher(content);
+        return m.find() ? m.group(1) : "";
+    }
+
+    /** The Java sources of the file's folder, for the constants a route takes 
from another class. */
+    private static Map<String, Supplier<String>> javaSources(Path file) throws 
IOException {
+        Map<String, Supplier<String>> answer = new LinkedHashMap<>();
+        Path dir = file.toAbsolutePath().getParent();
+        if (dir == null) {
+            return answer;
+        }
+        try (Stream<Path> list = Files.list(dir)) {
+            list.filter(p -> p.getFileName().toString().endsWith(".java"))
+                    .forEach(p -> answer.putIfAbsent(p.toString(), () -> {
+                        try {
+                            return Files.readString(p, StandardCharsets.UTF_8);
+                        } catch (IOException e) {
+                            return null;
+                        }
+                    }));
+        }
+        return answer;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
index 020f4d745822..e903a69a4cb6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
@@ -35,12 +35,13 @@ import org.apache.camel.util.StopWatch;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
 
-@Command(name = "route", description = "Transform Camel routes to XML or YAML 
format", sortOptions = false,
+@Command(name = "route", description = "Transform Camel routes to XML, YAML or 
Java format", sortOptions = false,
          showDefaultValues = true,
          footer = {
                  "%nExamples:",
                  "  camel transform route hello.java --format=yaml",
-                 "  camel transform route hello.xml --format=yaml" })
+                 "  camel transform route hello.xml --format=yaml",
+                 "  camel transform route hello.camel.yaml --format=java" })
 public class TransformRoute extends CamelCommand {
 
     public static class FormatCompletionCandidates implements Iterable<String> 
{
@@ -50,7 +51,7 @@ public class TransformRoute extends CamelCommand {
 
         @Override
         public Iterator<String> iterator() {
-            return List.of("xml", "yaml").iterator();
+            return List.of("xml", "yaml", "java").iterator();
         }
     }
 
@@ -67,7 +68,8 @@ public class TransformRoute extends CamelCommand {
 
     @CommandLine.Option(names = { "--format" },
                         completionCandidates = 
FormatCompletionCandidates.class,
-                        description = "Output format 
(${COMPLETION-CANDIDATES}), if only yaml files are provided, the format 
defaults to xml and vice versa")
+                        description = "Output format 
(${COMPLETION-CANDIDATES}), if only yaml files are provided, the format 
defaults to xml and vice versa."
+                                      + " Java is converted without running 
the routes, one route builder class per file")
     String format;
 
     @CommandLine.Option(names = { "--resolve-placeholders" }, defaultValue = 
"false",
@@ -105,6 +107,10 @@ public class TransformRoute extends CamelCommand {
             }
         }
 
+        if ("java".equals(format)) {
+            return transformToJava();
+        }
+
         String dump = output;
         // if no output then we want to print to console, so we need to write 
to a hidden file, and dump that file afterwards
         if (output == null || "clipboard".equals(output)) {
@@ -152,6 +158,41 @@ public class TransformRoute extends CamelCommand {
         return printDump(target);
     }
 
+    /**
+     * To Java, each file is converted without running it (CAMEL-25254): read 
into the model by the parser of its DSL
+     * and written as a route builder class, printed, or written into the 
output directory (or file, for one file).
+     */
+    private Integer transformToJava() throws Exception {
+        StringBuilder all = new StringBuilder();
+        Path out = output != null && !"clipboard".equals(output) ? 
Path.of(output) : null;
+        boolean toDirectory = out != null && (Files.isDirectory(out) || 
files.size() > 1);
+        for (String f : files) {
+            RouteDslConverter.Result r = RouteDslConverter.convert(Path.of(f), 
"java");
+            if (!r.converted()) {
+                printer().printErr(r.refused());
+                return 1;
+            }
+            // what did not carry over goes at the top of the class, as the 
TUI writes it
+            String java = RouteDslConverter.withNotes(r.content(), r.notes(), 
"java");
+            if (out == null) {
+                all.append(java).append('\n');
+            } else if (toDirectory) {
+                Files.createDirectories(out);
+                Files.writeString(out.resolve(r.fileName()), java);
+            } else {
+                Files.writeString(out, java);
+            }
+        }
+        if (out == null) {
+            if ("clipboard".equals(output)) {
+                StringSelection data = new StringSelection(all.toString());
+                
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(data, data);
+            }
+            printer().println(all.toString().stripTrailing());
+        }
+        return 0;
+    }
+
     private Integer printDump(String target) {
         if (output == null || "clipboard".equals(output)) {
             // load target file and print to console
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
new file mode 100644
index 000000000000..c23a8877ba8c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RouteDslConverterTest.java
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Converting a route file between YAML, XML and Java without running it 
(CAMEL-25254).
+ */
+class RouteDslConverterTest {
+
+    private static final String XML = """
+            <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                <!-- orders -->
+                <route id="orders">
+                    <from uri="kafka:orders"/>
+                    <choice>
+                        <when>
+                            <simple>${header.priority} == 'high'</simple>
+                            <log message="Urgent ${body}" loggingLevel="WARN"/>
+                        </when>
+                        <otherwise>
+                            <to uri="direct:normal"/>
+                        </otherwise>
+                    </choice>
+                </route>
+            </routes>
+            """;
+
+    private static final String YAML = """
+            - beans:
+                - name: orderService
+                  type: com.foo.OrderService
+                  properties:
+                    currency: EUR
+            - route:
+                id: orders
+                from:
+                  uri: kafka:orders
+                  steps:
+                    - split:
+                        simple: "${body}"
+                        steps:
+                          - to: direct:item
+            """;
+
+    private static RouteDslConverter.Result convert(String name, String 
content, String format) {
+        return RouteDslConverter.convert(name, content, format, Map.of());
+    }
+
+    @Test
+    void xmlToYaml() {
+        RouteDslConverter.Result r = convert("orders.camel.xml", XML, "yaml");
+        assertThat(r.converted()).isTrue();
+        assertThat(r.fileName()).isEqualTo("orders.camel.yaml");
+        assertThat(r.content()).contains("- route:", "id: orders", "uri: 
kafka:orders", "loggingLevel: WARN");
+        assertThat(r.notes()).containsExactly("The comments of 
orders.camel.xml are not carried over");
+    }
+
+    @Test
+    void xmlToAJavaClass() {
+        RouteDslConverter.Result r = convert("orders.camel.xml", XML, "java");
+        assertThat(r.converted()).isTrue();
+        assertThat(r.fileName()).isEqualTo("Orders.java");
+        assertThat(r.content()).contains("import 
org.apache.camel.LoggingLevel;",
+                "import org.apache.camel.builder.RouteBuilder;", "public class 
Orders extends RouteBuilder {",
+                "public void configure() throws Exception {", 
"from(\"kafka:orders\")",
+                ".log(LoggingLevel.WARN, \"Urgent ${body}\")");
+        // the converted class reads back to the same routes: no difference 
noted
+        assertThat(r.notes()).noneMatch(n -> n.startsWith("Check the 
converted"));
+    }
+
+    @Test
+    void yamlBeansAreConvertedWithoutCreatingThem() {
+        // com.foo.OrderService does not exist: the YAML DSL would fail 
creating it, the converter reads it as data
+        RouteDslConverter.Result r = convert("orders.camel.yaml", YAML, "xml");
+        assertThat(r.converted()).isTrue();
+        assertThat(r.content()).contains("<bean name=\"orderService\" 
type=\"com.foo.OrderService\">",
+                "<property key=\"currency\" value=\"EUR\"/>", "<split>", 
"<simple>${body}</simple>");
+
+        // Java has no beans in its routes: said, not dropped silently
+        r = convert("orders.camel.yaml", YAML, "java");
+        assertThat(r.converted()).isTrue();
+        assertThat(r.notes()).anyMatch(n -> n.contains("beans of 
orders.camel.yaml are not carried over"));
+    }
+
+    @Test
+    void javaToYamlWithoutRunningIt() {
+        String java = """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class OrderRoute extends RouteBuilder {
+                    @Override
+                    public void configure() {
+                        from("timer:tick").routeId("tick")
+                            .setBody(simple("Hello ${date:now}"))
+                            .to("log:out");
+                    }
+                }
+                """;
+        RouteDslConverter.Result r = convert("OrderRoute.java", java, "yaml");
+        assertThat(r.converted()).isTrue();
+        assertThat(r.fileName()).isEqualTo("OrderRoute.camel.yaml");
+        assertThat(r.content()).contains("id: tick", "uri: timer:tick", "Hello 
${date:now}");
+    }
+
+    @Test
+    void whatCannotBeConvertedIsRefusedWithAReason() {
+        String processor = """
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick").process(e -> 
e.getIn().setBody("x")).to("log:out");
+                    }
+                }
+                """;
+        assertThat(convert("MyRoute.java", processor, 
"yaml").refused()).contains("cannot be converted without running it");
+
+        String predicate = """
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        
from("timer:tick").filter(header("x").isEqualTo("y")).to("log:out");
+                    }
+                }
+                """;
+        assertThat(convert("MyRoute.java", predicate, 
"xml").refused()).contains("write it with simple first");
+
+        assertThat(convert("orders.camel.xml", XML, 
"xml").refused()).contains("is xml already");
+        assertThat(convert("notes.txt", "x", "yaml").refused()).contains("no 
YAML, XML or Java route file");
+    }
+
+    @Test
+    void semanticDeclarationsAreNotLostWithoutAWord() {
+        String yaml = """
+                - semantic:
+                    question:
+                      urgent:
+                        type: boolean
+                        instructions: Urgent?
+                - route:
+                    from:
+                      uri: direct:input
+                      steps:
+                        - log: Hello
+                """;
+        assertThat(convert("semantic.camel.yaml", yaml, 
"xml").refused()).contains("semantic declarations");
+
+        String java = """
+                import org.apache.camel.builder.RouteBuilder;
+                import static 
org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions;
+                public class SemanticRoute extends RouteBuilder {
+                    public void configure() {
+                        
semanticQuestions(this).question("urgent").type("boolean").register();
+                        from("direct:input").log("Hello");
+                    }
+                }
+                """;
+        // the parser cannot read the declaration: refused at its line, not 
dropped
+        assertThat(convert("SemanticRoute.java", java, 
"yaml").refused()).contains("SemanticRoute.java:5",
+                "cannot be converted without running it");
+    }
+
+    @Test
+    void theJavaClassImportsWhatItsRoutesUse() {
+        String routes = """
+                from("direct:a")
+                    
.setBody(expression().xpath("/t:ticket").namespaces(Map.of("t", 
"urn:tickets")).end())
+                    .log(LoggingLevel.WARN, "${body}");
+                """;
+        assertThat(RouteDslConverter.javaClass("Tickets", 
routes)).contains("import java.util.Map;",
+                "import org.apache.camel.LoggingLevel;", "import 
org.apache.camel.builder.RouteBuilder;")
+                .doesNotContain("ExchangePattern");
+    }
+
+    @Test
+    void namesOfTheConvertedFiles() {
+        assertThat(RouteDslConverter.targetName("orders.camel.yaml", 
"xml")).isEqualTo("orders.camel.xml");
+        assertThat(RouteDslConverter.targetName("my-routes.xml", 
"yaml")).isEqualTo("my-routes.camel.yaml");
+        assertThat(RouteDslConverter.targetName("my-routes.camel.xml", 
"java")).isEqualTo("MyRoutes.java");
+        assertThat(RouteDslConverter.targetName("OrderRoute.java", 
"yaml")).isEqualTo("OrderRoute.camel.yaml");
+        
assertThat(RouteDslConverter.javaClassName("1st")).isEqualTo("Route1st");
+    }
+
+    @Test
+    void theFirstDifferenceOfTwoDumps() {
+        assertThat(RouteDslConverter.firstDifference("<a/>\n<b/>", "<a/>\n  
<b/>")).isNull();
+        assertThat(RouteDslConverter.firstDifference("<a/>\n<b/>", 
"<a/>\n<c/>")).isEqualTo("expected <b/> but found <c/>");
+        // bean definitions go, the bean EIP of a route stays
+        String xml = """
+                <bean name="x" type="Foo">
+                    <properties>
+                        <property key="a" value="b"/>
+                    </properties>
+                </bean>
+                <route>
+                    <bean ref="x"/>
+                </route>
+                """;
+        assertThat(RouteDslConverter.withoutBeans(xml)).doesNotContain("Foo", 
"property").contains("<bean ref=\"x\"/>");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
index b8c58096c0ee..b34d154b5d09 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FileActionsPopup.java
@@ -36,6 +36,7 @@ import dev.tamboui.widgets.list.ListItem;
 import dev.tamboui.widgets.list.ListState;
 import dev.tamboui.widgets.list.ListWidget;
 import dev.tamboui.widgets.list.ScrollMode;
+import org.apache.camel.dsl.jbang.core.commands.RouteDslConverter;
 
 /**
  * File-actions menu for the Source tab file list (opened with F12). Presents 
basic file management (new file, new
@@ -50,7 +51,10 @@ class FileActionsPopup {
         RENAME,
         DUPLICATE,
         DELETE,
-        COPY_PATH
+        COPY_PATH,
+        CONVERT_YAML,
+        CONVERT_XML,
+        CONVERT_JAVA
     }
 
     /** A completed, ready-to-execute request. {@code name} is null for DELETE 
and COPY_PATH. */
@@ -71,6 +75,7 @@ class FileActionsPopup {
 
     private String targetName;
     private boolean hasTarget;
+    private boolean routeFile;
 
     private final ListState menuState = new ListState();
     private List<MenuItem> items = List.of();
@@ -83,6 +88,14 @@ class FileActionsPopup {
     private Request result;
 
     void open(String selectedName, boolean hasTarget) {
+        open(selectedName, hasTarget, false);
+    }
+
+    /**
+     * Opens the menu for the selected entry; a route file can also be 
converted to the other DSLs (CAMEL-25254).
+     */
+    void open(String selectedName, boolean hasTarget, boolean routeFile) {
+        this.routeFile = routeFile;
         this.visible = true;
         this.phase = Phase.MENU;
         this.targetName = selectedName;
@@ -118,6 +131,19 @@ class FileActionsPopup {
             list.add(new MenuItem(Action.DUPLICATE, TuiIcons.DUPLICATE, 
"Duplicate…"));
             list.add(new MenuItem(Action.DELETE, TuiIcons.DELETE, "Delete"));
             list.add(new MenuItem(Action.COPY_PATH, TuiIcons.CLIPBOARD, "Copy 
path to clipboard"));
+            String format = routeFile ? RouteDslConverter.formatOf(targetName) 
: null;
+            if (format != null) {
+                // to the other two DSLs, written next to the file
+                if (!"yaml".equals(format)) {
+                    list.add(new MenuItem(Action.CONVERT_YAML, 
TuiIcons.CONVERT, "Convert to YAML…"));
+                }
+                if (!"xml".equals(format)) {
+                    list.add(new MenuItem(Action.CONVERT_XML, 
TuiIcons.CONVERT, "Convert to XML…"));
+                }
+                if (!"java".equals(format)) {
+                    list.add(new MenuItem(Action.CONVERT_JAVA, 
TuiIcons.CONVERT, "Convert to Java…"));
+                }
+            }
         }
         this.items = list;
     }
@@ -166,8 +192,8 @@ class FileActionsPopup {
             case RENAME -> startInput(action, "Rename", targetName);
             case DUPLICATE -> startInput(action, "Duplicate", 
SourceFileOps.suggestDuplicateName(targetName));
             case DELETE -> phase = Phase.CONFIRM;
-            case COPY_PATH -> {
-                result = new Request(Action.COPY_PATH, null);
+            case COPY_PATH, CONVERT_YAML, CONVERT_XML, CONVERT_JAVA -> {
+                result = new Request(action, null);
                 close();
             }
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 7131d9ada5dc..d05c6f25b018 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -58,6 +58,7 @@ import dev.tamboui.widgets.list.ScrollMode;
 import dev.tamboui.widgets.paragraph.Paragraph;
 import dev.tamboui.widgets.scrollbar.Scrollbar;
 import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.dsl.jbang.core.commands.RouteDslConverter;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -736,7 +737,9 @@ class SourceTab extends AbstractTab {
         }
         FilesBrowser.FileEntry entry = selectedEntry();
         boolean hasTarget = entry != null && !"..".equals(entry.name());
-        fileActionsPopup.open(hasTarget ? entry.name() : null, hasTarget);
+        boolean routeFile = hasTarget && !entry.directory()
+                && (isCamelSourceFile(Path.of(entry.path())) || 
isJavaRouteFile(Path.of(entry.path())));
+        fileActionsPopup.open(hasTarget ? entry.name() : null, hasTarget, 
routeFile);
     }
 
     private FilesBrowser.FileEntry selectedEntry() {
@@ -796,6 +799,9 @@ class SourceTab extends AbstractTab {
                     TuiHelper.copyToClipboard(entry.path());
                     notify("Copied path to clipboard", false);
                 }
+                case CONVERT_YAML -> convert(entry, "yaml");
+                case CONVERT_XML -> convert(entry, "xml");
+                case CONVERT_JAVA -> convert(entry, "java");
             }
         } catch (Exception e) {
             notify(e.getMessage() != null ? e.getMessage() : e.toString(), 
true);
@@ -808,6 +814,31 @@ class SourceTab extends AbstractTab {
         }
     }
 
+    /**
+     * Converts the route file to another DSL without running it 
(CAMEL-25254), into a new file next to it which opens;
+     * what does not carry over is said at the top of the new file. An 
existing file is not overwritten.
+     */
+    private void convert(FilesBrowser.FileEntry entry, String format) throws 
IOException {
+        if (entry == null) {
+            return;
+        }
+        RouteDslConverter.Result r = 
RouteDslConverter.convert(Path.of(entry.path()), format);
+        if (!r.converted()) {
+            notify(r.refused(), true);
+            return;
+        }
+        Path target = currentDir.resolve(r.fileName());
+        if (Files.exists(target)) {
+            notify(r.fileName() + " exists already: rename or delete it 
first", true);
+            return;
+        }
+        Files.writeString(target, RouteDslConverter.withNotes(r.content(), 
r.notes(), format), StandardCharsets.UTF_8);
+        if (loadDirectory(currentDir, r.fileName())) {
+            openSelectedEntry();
+        }
+        notify("Converted to " + r.fileName() + (r.notes().isEmpty() ? "" : ", 
see the notes at its top"), false);
+    }
+
     private void openSelectedEntry() {
         if (sourceViewer.isEditMode()) {
             return;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index f3ee3e6a738d..1308442ab22e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -62,6 +62,7 @@ final class TuiIcons {
     // memo (📝) reads as "edit"; the letters glyph reads as changing the name
     static final String RENAME = "🔤";
     static final String DUPLICATE = "📑";
+    static final String CONVERT = "🔀";
     static final String DELETE = "🗑️";
 
     // ---- Actions menu ----
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 2427517cebfb..c8646fc8bdd1 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -8,7 +8,9 @@ explorer on the left and a source viewer on the right.
 - **Up/Down** — navigate files
 - **Enter** — open file or directory
 - **F4** — open file directly in edit mode
-- **F12** — file actions menu (new file, new folder, rename, duplicate, 
delete, copy path)
+- **F12** — file actions menu (new file, new folder, rename, duplicate, 
delete, copy path,
+  and for a route file, convert to YAML, XML or Java: a new file next to it,
+  without running it, with what did not carry over noted at its top)
 - **Backspace** — go to parent directory
 
 ## Source Viewer (right panel)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConvertTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConvertTest.java
new file mode 100644
index 000000000000..d165ea03aaec
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConvertTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.dsl.jbang.core.commands.RouteDslConverter;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Converting a route file to another DSL from the file actions of the Source 
tab (CAMEL-25254).
+ */
+class SourceConvertTest {
+
+    /** The action chosen after moving down the menu the given number of 
times. */
+    private static FileActionsPopup.Action choose(String file, boolean 
routeFile, int downs) {
+        FileActionsPopup popup = new FileActionsPopup();
+        popup.open(file, true, routeFile);
+        for (int i = 0; i < downs; i++) {
+            popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        FileActionsPopup.Request r = popup.consumeResult();
+        return r != null ? r.action() : null;
+    }
+
+    @Test
+    void aRouteFileConvertsToTheOtherTwoDsls() {
+        // new file, new folder, rename, duplicate, delete, copy path, then 
the conversions
+        assertThat(choose("orders.camel.xml", true, 
6)).isEqualTo(FileActionsPopup.Action.CONVERT_YAML);
+        assertThat(choose("orders.camel.xml", true, 
7)).isEqualTo(FileActionsPopup.Action.CONVERT_JAVA);
+        assertThat(choose("orders.camel.yaml", true, 
6)).isEqualTo(FileActionsPopup.Action.CONVERT_XML);
+        assertThat(choose("OrderRoute.java", true, 
6)).isEqualTo(FileActionsPopup.Action.CONVERT_YAML);
+        assertThat(choose("OrderRoute.java", true, 
7)).isEqualTo(FileActionsPopup.Action.CONVERT_XML);
+    }
+
+    @Test
+    void otherFilesHaveNoConversion() {
+        // the menu ends at copy path: moving further stays on it
+        assertThat(choose("pom.xml", false, 
9)).isEqualTo(FileActionsPopup.Action.COPY_PATH);
+    }
+
+    @Test
+    void theNotesGoAtTheTopOfTheConvertedFile() {
+        List<String> notes = List.of("The comments of orders.camel.xml are not 
carried over");
+        assertThat(RouteDslConverter.withNotes("- route:\n", notes, "yaml"))
+                .startsWith("# The comments of orders.camel.xml are not 
carried over\n- route:");
+        assertThat(RouteDslConverter.withNotes("<camel/>\n", notes, "xml"))
+                .startsWith("<!--\n  The comments of orders.camel.xml are not 
carried over\n-->\n<camel/>");
+        assertThat(RouteDslConverter.withNotes("import x;\n", notes, "java"))
+                .startsWith("// The comments of orders.camel.xml are not 
carried over\nimport x;");
+        assertThat(RouteDslConverter.withNotes("- route:\n", List.of(), 
"yaml")).isEqualTo("- route:\n");
+    }
+}

Reply via email to