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

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

commit da2929b7369a6936093fdd72b1bfc8aeba5923b8
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 11:54:18 2026 +0200

    CAMEL-25565: camel-jbang - route-diagram, route-structure and 
route-topology read Java source files with the Java DSL parser instead of 
compiling them
    
    Java source files are read by the Java DSL parser and their route structure
    and topology JSON is dumped from a CamelContext that is never started, as
    camel transform route does. A step the parser cannot work out is shown as
    its placeholder and the parts it could not read are listed. The new
    --compile option compiles the sources as before.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 design/java-dsl-parser.adoc                        |   5 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  22 +++
 .../modules/ROOT/pages/camel-4x-upgrade-guide.adoc |   1 +
 .../ROOT/pages/camel-jbang-diagnostics.adoc        |   3 +-
 .../camel-jbang-cmd-route-diagram.adoc             |   1 +
 .../camel-jbang-cmd-route-structure.adoc           |   1 +
 .../camel-jbang-cmd-route-topology.adoc            |   1 +
 .../modules/ROOT/pages/route-diagram.adoc          |   5 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../jbang/core/commands/TransformJavaRoutes.java   | 105 ++++++++++++--
 .../core/commands/action/ActionBaseCommand.java    |  49 ++++++-
 .../commands/action/CamelRouteDiagramAction.java   |  14 +-
 .../commands/action/CamelRouteStructureAction.java |  13 ++
 .../commands/action/CamelRouteTopologyAction.java  |  11 +-
 .../commands/action/RouteActionJavaSourceTest.java | 161 +++++++++++++++++++++
 .../jbang/core/commands/mcp/RouteDiagramTools.java |  16 +-
 16 files changed, 378 insertions(+), 32 deletions(-)

diff --git a/design/java-dsl-parser.adoc b/design/java-dsl-parser.adoc
index 80ee524bbfd2..8c021b46e5ac 100644
--- a/design/java-dsl-parser.adoc
+++ b/design/java-dsl-parser.adoc
@@ -108,6 +108,11 @@ camel-jbang reads Java routes with the parser only; the 
pattern matching on stri
 running Camel, in a CamelContext that is never started (CAMEL-25202); it 
compiles the routes only when one cannot be
 read completely, or with `--compile`.
 
+`camel cmd route-diagram`, `route-topology` and `route-structure` (and the 
`camel_render_route_diagram` MCP tool) read
+Java source files the same way, and dump the route structure and topology JSON 
from that CamelContext (CAMEL-25565). A
+route read only partly is still shown: a lambda or a processor is the 
placeholder step, and the parts the parser could
+not read are listed. They compile the routes only with `--compile`.
+
 == Security
 
 The parser reads sources nobody vetted: a cloned example opened in the TUI, 
documentation, a project an AI agent
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
new file mode 100644
index 000000000000..28e1811e7adb
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -0,0 +1,22 @@
+= Apache Camel 4.x Upgrade Guide
+
+This document is for helping you upgrade your Apache Camel application
+from Camel 4.x to 4.y. For example, if you are upgrading Camel 4.0 to 4.2, 
then you should follow the guides
+from both 4.0 to 4.1 and 4.1 to 4.2.
+
+[NOTE]
+====
+https://github.com/apache/camel-upgrade-recipes/[The Camel Upgrade Recipes 
project] provides automated assistance for some common migration tasks.
+Note that manual migration is still required.
+See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details.
+====
+
+== Upgrading Camel 4.23 to 4.24
+
+=== camel-jbang - route-diagram, route-structure and route-topology read Java 
sources without compiling them
+
+Given Java source files, `camel cmd route-diagram`, `camel cmd 
route-structure` and `camel cmd route-topology` (and the
+`camel_render_route_diagram` MCP tool) now read the routes with the Java DSL 
parser, as `camel transform route` and
+`camel overview` do, instead of compiling the sources and loading their routes 
in a short-lived Camel. A step the parser
+cannot work out, such as a lambda or a processor, is shown as a placeholder, 
and the parts it could not read are
+listed. Use the new `--compile` option to compile the sources and load their 
routes as before.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
index 982e18bb2fba..91f8f833c7fe 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
@@ -32,6 +32,7 @@ You can find the upgrade guide for each release in the 
following pages:
 - xref:camel-4x-upgrade-guide-4_21.adoc[Upgrade guide 4.20 -> 4.21]
 - xref:camel-4x-upgrade-guide-4_22.adoc[Upgrade guide 4.21 -> 4.22 LTS]
 - xref:camel-4x-upgrade-guide-4_23.adoc[Upgrade guide 4.22 -> 4.23]
+- xref:camel-4x-upgrade-guide-4_24.adoc[Upgrade guide 4.23 -> 4.24]
 
 [NOTE]
 ====
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
index 8ed30eb3de6b..1613e5130bb7 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
@@ -180,7 +180,8 @@ camel cmd route-structure myApp --filter=myRoute --brief
 camel cmd route-structure myApp --description
 ----
 
-Give a source file instead of a running integration to see the structure 
without running it.
+Give a source file instead of a running integration to see the structure 
without running it. A Java source file is read
+by the Java DSL parser, without compiling it; use `--compile` to compile it 
instead.
 `--brief` shortens each node, and `--description` shows the description of 
each node instead of its code.
 
 === Route dump
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
index dbcb7566d452..01ab81301517 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
@@ -20,6 +20,7 @@ camel cmd route-diagram [options]
 |===
 | Option | Description | Default | Type
 | `--box-width` | Node box width in logical pixels | 180 | int
+| `--compile` | Whether to compile Java source files and load their routes. By 
default Java routes are read by the Java DSL parser, without compiling them | 
false | boolean
 | `--filter` | Filter route by filename or route id |  | String
 | `--font-size` | Font size in logical pixels for node text | 12 | int
 | `--highlight` | Comma-separated node IDs to highlight in the diagram |  | 
String
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
index 8ce1fdceb2b5..187e045a7618 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
@@ -20,6 +20,7 @@ camel cmd route-structure [options]
 |===
 | Option | Description | Default | Type
 | `--brief` | To show less detailed route structure |  | boolean
+| `--compile` | Whether to compile Java source files and load their routes. By 
default Java routes are read by the Java DSL parser, without compiling them | 
false | boolean
 | `--description` | To show description instead of code |  | boolean
 | `--filter` | Filter route by filename or route id (multiple names can be 
separated by comma) |  | String
 | `--ignore-loading-error` | Whether to ignore route loading and compilation 
errors (use this with care!) | false | boolean
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
index be5d95e0a598..072b56b995f1 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
@@ -20,6 +20,7 @@ camel cmd route-topology [options]
 |===
 | Option | Description | Default | Type
 | `--box-width` | Width of diagram node boxes | 180 | int
+| `--compile` | Whether to compile Java source files and load their routes. By 
default Java routes are read by the Java DSL parser, without compiling them | 
false | boolean
 | `--description` | Prefer route description over route id in node labels |  | 
boolean
 | `--external` | Include external systems (consumers at top, producers at 
bottom) |  | boolean
 | `--font-size` | Font size in logical pixels for node text | 12 | int
diff --git a/docs/user-manual/modules/ROOT/pages/route-diagram.adoc 
b/docs/user-manual/modules/ROOT/pages/route-diagram.adoc
index 72a7261b8317..8c7b218f2fc9 100644
--- a/docs/user-manual/modules/ROOT/pages/route-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/route-diagram.adoc
@@ -27,6 +27,11 @@ It also works with Java source files:
 camel cmd route-diagram MyRoute.java
 ----
 
+Java routes are read by the Java DSL parser, without compiling them, so the 
diagram is ready in milliseconds and the
+source does not need its dependencies. A step the parser cannot work out, such 
as a lambda or a processor, is shown as a
+placeholder, and the parts it could not read are listed. Use `--compile` to 
compile the source and load its routes
+instead. The same applies to `camel cmd route-topology` and `camel cmd 
route-structure`.
+
 And you can include multiple files:
 
 [source,bash]
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 36ef5dcfb69e..34e0252a4a95 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
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--api-type", "description": 
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType":  [...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
index 1172cf28159d..3195b87dff3c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
@@ -27,32 +27,43 @@ import java.util.Map;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
 
+import org.apache.camel.NamedNode;
 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.RouteDefinition;
 import org.apache.camel.spi.Resource;
 import org.apache.camel.support.ResourceHelper;
 
 /**
- * {@code camel transform route} for Java routes without compiling them 
(CAMEL-25202): the sources are read into the
- * model by the Java DSL parser of camel-java-io, and dumped as YAML or XML by 
the same dump strategy a running Camel
- * uses ({@code camel.main.dumpRoutes}), in a CamelContext that is never 
started. Nothing of the project is compiled,
- * loaded or run.
+ * Java routes read without compiling them: the sources are read into the 
model by the Java DSL parser of camel-java-io,
+ * and dumped by the same dump strategy a running Camel uses ({@code 
camel.main.dumpRoutes}), in a CamelContext that is
+ * never started. Nothing of the project is compiled, loaded or run.
+ * <p>
+ * Used by {@code camel transform route} (as YAML or XML, CAMEL-25202), and by 
{@code camel cmd route-diagram},
+ * {@code route-structure} and {@code route-topology} (as the route structure 
and topology JSON, CAMEL-25565).
  */
-final class TransformJavaRoutes {
+public final class TransformJavaRoutes {
 
-    /** Whether the routes were transformed, and why not: the first part the 
parser could not read. */
-    record Result(boolean transformed, String reason) {
+    /**
+     * Whether the routes were dumped, and why not; and the parts the parser 
could not read (empty when every route was
+     * read completely).
+     */
+    public record Result(boolean transformed, String reason, List<String> 
unread) {
+    }
+
+    /** The routes read from the files, and the parts of them the parser could 
not read. */
+    private record Read(List<JavaParseResult> results, List<Resource> 
resources, List<String> unread, String failure) {
     }
 
     private TransformJavaRoutes() {
     }
 
     /** Whether the files are all Java sources, which the parser may read. */
-    static boolean applies(List<String> files) {
+    public static boolean applies(List<String> files) {
         return !files.isEmpty()
                 && files.stream().allMatch(f -> f.endsWith(".java") && 
Files.isRegularFile(Path.of(f)));
     }
@@ -67,33 +78,86 @@ final class TransformJavaRoutes {
      */
     static Result transform(List<String> files, String format, String output, 
boolean uriAsParameters)
             throws Exception {
+        Read read = read(files, true);
+        if (read.failure() != null) {
+            return new Result(false, read.failure(), read.unread());
+        }
+        dump(read, format, output, uriAsParameters);
+        return new Result(true, null, List.of());
+    }
+
+    /**
+     * Dumps the route structure and the route topology of the Java routes of 
the files as JSON, as
+     * {@code camel.main.dumpRoutes=json} does: one route structure file per 
source file, and
+     * {@code route-topology.json}. A route the parser reads only partly is 
dumped too: a step it cannot work out (a
+     * lambda, a processor) is the placeholder the parser puts in the model, 
and the parts it could not read are
+     * returned.
+     *
+     * @param  output the directory (or file) to write to
+     * @return        whether they were dumped; else why not (no file has a 
route the parser can read)
+     */
+    public static Result dumpStructure(List<String> files, String output) 
throws Exception {
+        Read read = read(files, false);
+        if (read.failure() != null) {
+            return new Result(false, read.failure(), read.unread());
+        }
+        dump(read, "json", output, false);
+        return new Result(true, null, read.unread());
+    }
+
+    /**
+     * Reads the routes of the files.
+     *
+     * @param complete whether every route must be read completely: else the 
first part the parser could not read is the
+     *                 failure; when not, a file without routes is skipped, 
and the failure is that no file has one
+     */
+    private static Read read(List<String> files, boolean complete) throws 
Exception {
         CamelCatalog catalog = new DefaultCamelCatalog();
         Map<String, Supplier<String>> sources = javaSources(files);
         List<JavaParseResult> results = new ArrayList<>();
         List<Resource> resources = new ArrayList<>();
+        List<String> unread = new ArrayList<>();
         for (String f : files) {
             String content = Files.readString(Path.of(f), 
StandardCharsets.UTF_8);
             JavaParseResult result = ProjectRoutes.parseJava(content, sources, 
catalog);
             for (JavaParseResult.Unresolved u : result.unresolved()) {
                 if (!JavaParseResult.configuresTheContext(u)) {
-                    return new Result(false, f + ":" + u.line() + " " + 
u.text() + " (" + u.reason() + ")");
+                    String part = f + ":" + u.line() + " " + u.text() + " (" + 
u.reason() + ")";
+                    if (complete) {
+                        return new Read(List.of(), List.of(), List.of(part), 
part);
+                    }
+                    unread.add(part);
                 }
             }
             if (result.routes().getRoutes().isEmpty() && 
result.rests().getRests().isEmpty()
                     && result.routeTemplates().getRouteTemplates().isEmpty()
                     && 
result.routeConfigurations().getRouteConfigurations().isEmpty()) {
-                return new Result(false, f + " has no routes the parser can 
read");
+                if (complete) {
+                    return new Read(List.of(), List.of(), List.of(), f + " has 
no routes the parser can read");
+                }
+                continue;
             }
             results.add(result);
             // the source file, so each is dumped on its own and named after 
it, as when the routes run
             resources.add(ResourceHelper.fromString("file:" + 
Path.of(f).getFileName(), content));
         }
+        if (results.isEmpty()) {
+            return new Read(List.of(), List.of(), unread, String.join(", ", 
files) + " has no routes the parser can read");
+        }
+        return new Read(results, resources, unread, null);
+    }
+
+    private static void dump(Read read, String format, String output, boolean 
uriAsParameters) throws Exception {
         try (DefaultCamelContext context = new DefaultCamelContext()) {
             // never started: adding the definitions only registers them
-            for (int i = 0; i < results.size(); i++) {
-                JavaParseResult r = results.get(i);
-                Resource resource = resources.get(i);
+            for (int i = 0; i < read.results().size(); i++) {
+                JavaParseResult r = read.results().get(i);
+                Resource resource = read.resources().get(i);
                 r.routes().getRoutes().forEach(d -> d.setResource(resource));
+                if ("json".equals(format)) {
+                    // the source of each step, as a routes loader records it 
(the route structure shows it)
+                    r.routes().getRoutes().forEach(d -> locate(d, 
resource.getLocation()));
+                }
                 r.rests().getRests().forEach(d -> d.setResource(resource));
                 r.routeTemplates().getRouteTemplates().forEach(d -> 
d.setResource(resource));
                 r.routeConfigurations().getRouteConfigurations().forEach(d -> 
d.setResource(resource));
@@ -111,7 +175,20 @@ final class TransformJavaRoutes {
             dumper.setOutput(output);
             dumper.dumpRoutes(format);
         }
-        return new Result(true, null);
+    }
+
+    /** Sets the source location of the node and its children (the line 
numbers the parser has set already). */
+    private static void locate(NamedNode node, String location) {
+        if (node.getLocation() == null) {
+            node.setLocation(location);
+        }
+        if (node instanceof RouteDefinition route && route.getInput() != null) 
{
+            route.getInput().setLocation(location);
+        }
+        List<? extends NamedNode> children = node.getChildren();
+        if (children != null) {
+            children.forEach(c -> locate(c, location));
+        }
     }
 
     /** The Java sources of the files' folders, for the constants a route 
takes from another class. */
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
index cf9bcea3b3c0..87686da6a5f2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
@@ -28,6 +28,7 @@ import java.util.regex.Pattern;
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.Run;
+import org.apache.camel.dsl.jbang.core.commands.TransformJavaRoutes;
 import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
@@ -189,18 +190,22 @@ abstract class ActionBaseCommand extends CamelCommand {
     }
 
     /**
-     * Boots the given source files through a transient Camel run (see {@link 
Run#runTransform(boolean)}) that dumps the
-     * route structure as JSON into a temporary work directory, so 
route-diagram / route-topology can render from source
-     * files without a running integration. The caller reads the dumped JSON 
from {@link SourceDump#workDir()} and is
-     * responsible for deleting that directory when done.
+     * Dumps the route structure of the given source files as JSON into a 
temporary work directory, so route-diagram /
+     * route-topology can render from source files without a running 
integration. Java sources are read by the Java DSL
+     * parser (see {@link TransformJavaRoutes#dumpStructure(List, String)}) 
without compiling them, unless
+     * {@code compile} is set; other sources, or Java sources with {@code 
compile}, are booted through a transient Camel
+     * run (see {@link Run#runTransform(boolean)}). The caller reads the 
dumped JSON from {@link SourceDump#workDir()}
+     * and is responsible for deleting that directory when done.
      *
      * @param  sourceFiles        the source files to load
      * @param  workDirPrefix      prefix for the (pid-suffixed) temporary work 
directory name
      * @param  ignoreLoadingError whether to ignore route loading and 
compilation errors
-     * @return                    the dump location and the instant the boot 
started, or {@code null} if a file does not
-     *                            exist or the boot failed (an error is 
already printed to the user in that case)
+     * @param  compile            whether to compile Java sources and boot 
them, instead of reading them with the parser
+     * @return                    the dump location and the instant the dump 
started, or {@code null} if a file does not
+     *                            exist or the dump failed (an error is 
already printed to the user in that case)
      */
-    protected SourceDump dumpRoutesFromSource(List<String> sourceFiles, String 
workDirPrefix, boolean ignoreLoadingError)
+    protected SourceDump dumpRoutesFromSource(
+            List<String> sourceFiles, String workDirPrefix, boolean 
ignoreLoadingError, boolean compile)
             throws Exception {
         for (String name : sourceFiles) {
             File f = new File(name);
@@ -217,6 +222,10 @@ abstract class ActionBaseCommand extends CamelCommand {
         // previous --watch iteration whose cleanup above raced with (or lost 
against) a slow filesystem
         Instant bootStart = Instant.now();
 
+        if (!compile && TransformJavaRoutes.applies(sourceFiles)) {
+            return dumpJavaRoutes(sourceFiles, target) ? new 
SourceDump(workDir, bootStart) : null;
+        }
+
         Run run = new Run(getMain()) {
             @Override
             protected void doAddInitialProperty(KameletMain main) {
@@ -242,6 +251,32 @@ abstract class ActionBaseCommand extends CamelCommand {
         return new SourceDump(workDir, bootStart);
     }
 
+    /**
+     * Dumps the route structure and topology JSON of Java sources read by the 
Java DSL parser, and prints what the
+     * parser could not read.
+     *
+     * @return whether the routes were dumped (else an error is already 
printed)
+     */
+    protected boolean dumpJavaRoutes(List<String> sourceFiles, String target) 
throws Exception {
+        TransformJavaRoutes.Result result = 
TransformJavaRoutes.dumpStructure(sourceFiles, target);
+        if (!result.transformed()) {
+            printer().printErr("No routes found: " + result.reason()
+                               + " (use --compile to compile the source and 
load its routes)");
+            return false;
+        }
+        if (!result.unread().isEmpty() && printSourceNotes()) {
+            printer().println("Routes read without compiling them; the parser 
could not work out these parts "
+                              + "(use --compile to compile the source 
instead):");
+            result.unread().forEach(u -> printer().println("  " + u));
+        }
+        return true;
+    }
+
+    /** Whether to print the parts of Java sources the parser could not read: 
not when the output is JSON. */
+    protected boolean printSourceNotes() {
+        return true;
+    }
+
     /**
      * Result of {@link #dumpRoutesFromSource}: the temporary directory the 
route JSON was dumped into, and the instant
      * the transient boot started (used to tell a fresh dump apart from a 
stale one left by an earlier run).
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index 8dde047f6827..0ec36186485f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -126,6 +126,11 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
                         description = "Whether to ignore route loading and 
compilation errors (use this with care!)")
     boolean ignoreLoadingError;
 
+    @CommandLine.Option(names = { "--compile" }, defaultValue = "false",
+                        description = "Whether to compile Java source files 
and load their routes. By default Java "
+                                      + "routes are read by the Java DSL 
parser, without compiling them")
+    boolean compile;
+
     private long pid;
 
     private DiagramColors colors;
@@ -371,13 +376,12 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
     }
 
     /**
-     * Loads the given source files via a transient Camel boot (see
-     * {@link #dumpRoutesFromSource(List, String, boolean)}) and returns the 
merged route structure across all of them,
-     * or {@code null} if a file does not exist or loading failed (an error is 
already printed to the user in that
-     * case). Package visible for testing.
+     * Loads the given source files (see {@link #dumpRoutesFromSource(List, 
String, boolean, boolean)}) and returns the
+     * merged route structure across all of them, or {@code null} if a file 
does not exist or loading failed (an error
+     * is already printed to the user in that case). Package visible for 
testing.
      */
     List<RouteInfo> doCallSource(List<String> sourceFiles) throws Exception {
-        SourceDump dump = dumpRoutesFromSource(sourceFiles, 
"route-diagram-source-", ignoreLoadingError);
+        SourceDump dump = dumpRoutesFromSource(sourceFiles, 
"route-diagram-source-", ignoreLoadingError, compile);
         if (dump == null) {
             return null;
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
index fc8fcc793a05..1e66be82b1d7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
@@ -26,6 +26,7 @@ import java.util.Objects;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.Run;
+import org.apache.camel.dsl.jbang.core.commands.TransformJavaRoutes;
 import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
@@ -88,6 +89,11 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
                         description = "Whether to ignore route loading and 
compilation errors (use this with care!)")
     boolean ignoreLoadingError;
 
+    @CommandLine.Option(names = { "--compile" }, defaultValue = "false",
+                        description = "Whether to compile Java source files 
and load their routes. By default Java "
+                                      + "routes are read by the Java DSL 
parser, without compiling them")
+    boolean compile;
+
     @CommandLine.Option(names = { "--sort" }, completionCandidates = 
NameIdCompletionCandidates.class,
                         description = "Sort route by name or id", defaultValue 
= "name")
     String sort;
@@ -224,6 +230,9 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
         }
 
         final String target = CommandLineHelper.CAMEL_JBANG_WORK_DIR + 
"/structure-output.json";
+        if (!compile && TransformJavaRoutes.applies(List.of(name))) {
+            return dumpJavaRoutes(List.of(name), target) ? 0 : 1;
+        }
 
         Run run = new Run(getMain()) {
             @Override
@@ -339,4 +348,8 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
         String description;
     }
 
+    @Override
+    protected boolean printSourceNotes() {
+        return !jsonOutput;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
index 995711824925..8d9c1ecb34cd 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
@@ -109,6 +109,11 @@ public class CamelRouteTopologyAction extends 
ActionBaseCommand {
                         description = "Whether to ignore route loading and 
compilation errors (use this with care!)")
     boolean ignoreLoadingError;
 
+    @CommandLine.Option(names = { "--compile" }, defaultValue = "false",
+                        description = "Whether to compile Java source files 
and load their routes. By default Java "
+                                      + "routes are read by the Java DSL 
parser, without compiling them")
+    boolean compile;
+
     private long pid;
 
     public CamelRouteTopologyAction(CamelJBangMain main) {
@@ -154,7 +159,7 @@ public class CamelRouteTopologyAction extends 
ActionBaseCommand {
     }
 
     private int doCallSource(List<String> sourceFiles) throws Exception {
-        SourceDump dump = dumpRoutesFromSource(sourceFiles, 
"route-topology-source-", ignoreLoadingError);
+        SourceDump dump = dumpRoutesFromSource(sourceFiles, 
"route-topology-source-", ignoreLoadingError, compile);
         if (dump == null) {
             return 1;
         }
@@ -344,4 +349,8 @@ public class CamelRouteTopologyAction extends 
ActionBaseCommand {
         }
     }
 
+    @Override
+    protected boolean printSourceNotes() {
+        return !jsonOutput;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
new file mode 100644
index 000000000000..f7e635f9dafd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * route-diagram, route-topology and route-structure read Java source files 
with the Java DSL parser (CAMEL-25565): the
+ * routes are dumped from a CamelContext that is never started, so nothing is 
compiled and a source that does not
+ * compile (here: a processor class that does not exist) still gets its 
diagram.
+ */
+class RouteActionJavaSourceTest {
+
+    private static final String ORDERS = """
+            import org.apache.camel.builder.RouteBuilder;
+
+            public class Orders extends RouteBuilder {
+                @Override
+                public void configure() {
+                    from("timer:orders").routeId("intake")
+                        .setBody(constant("order"))
+                        .process(exchange -> 
exchange.getMessage().setHeader("checked", true))
+                        .to("direct:billing");
+                }
+            }
+            """;
+
+    private static final String BILLING = """
+            import org.apache.camel.builder.RouteBuilder;
+
+            public class Billing extends RouteBuilder {
+                @Override
+                public void configure() {
+                    from("direct:billing").routeId("billing")
+                        .process(new NoSuchProcessor())
+                        .log("${body}");
+                }
+            }
+            """;
+
+    @Test
+    void diagramReadsJavaRoutesWithoutCompilingThem(@TempDir Path dir) throws 
Exception {
+        Path orders = write(dir, "Orders.java", ORDERS);
+        Path billing = write(dir, "Billing.java", BILLING);
+        StringPrinter printer = new StringPrinter();
+        CamelRouteDiagramAction command = new CamelRouteDiagramAction(new 
CamelJBangMain().withPrinter(printer));
+
+        List<RouteInfo> routes = 
command.doCallSource(List.of(orders.toString(), billing.toString()));
+
+        assertThat(routes).extracting(r -> 
r.routeId).containsExactlyInAnyOrder("intake", "billing");
+        RouteInfo intake = routes.stream().filter(r -> 
"intake".equals(r.routeId)).findFirst().orElseThrow();
+        // the lambda is a placeholder step, so the route keeps all its steps
+        assertThat(intake.nodes).extracting(n -> n.type).contains("from", 
"setBody", "process", "to");
+        assertThat(printer.getOutput())
+                .contains("Routes read without compiling them")
+                .contains("Orders.java:")
+                .contains("a lambda or method reference");
+    }
+
+    @Test
+    void diagramCompilesWithCompileOption(@TempDir Path dir) throws Exception {
+        Path missing = dir.resolve("Missing.java");
+        StringPrinter printer = new StringPrinter();
+        CamelRouteDiagramAction command = new CamelRouteDiagramAction(new 
CamelJBangMain().withPrinter(printer));
+        command.compile = true;
+
+        // the existence check comes first, also when compiling
+        assertThat(command.doCallSource(List.of(missing.toString()))).isNull();
+        assertThat(printer.getOutput()).contains("File does not exist: " + 
missing);
+    }
+
+    @Test
+    void diagramReportsJavaSourceWithoutRoutes(@TempDir Path dir) throws 
Exception {
+        Path helper = write(dir, "Helper.java", """
+                public class Helper {
+                    public static String name() {
+                        return "helper";
+                    }
+                }
+                """);
+        StringPrinter printer = new StringPrinter();
+        CamelRouteDiagramAction command = new CamelRouteDiagramAction(new 
CamelJBangMain().withPrinter(printer));
+
+        assertThat(command.doCallSource(List.of(helper.toString()))).isNull();
+        assertThat(printer.getOutput())
+                .contains("No routes found")
+                .contains("Helper.java has no routes the parser can read")
+                .contains("--compile");
+    }
+
+    @Test
+    void topologyLinksJavaRoutesOfSeveralFiles(@TempDir Path dir) throws 
Exception {
+        Path orders = write(dir, "Orders.java", ORDERS);
+        Path billing = write(dir, "Billing.java", BILLING);
+        StringPrinter printer = new StringPrinter();
+        CamelRouteTopologyAction command = new CamelRouteTopologyAction(new 
CamelJBangMain().withPrinter(printer));
+        command.files = List.of(orders.toString(), billing.toString());
+        command.jsonOutput = true;
+
+        int exit = command.doCall();
+
+        assertThat(exit).isZero();
+        // with --json only the JSON is printed, not the parts the parser 
could not read
+        JsonObject topology = (JsonObject) 
Jsoner.deserialize(printer.getOutput());
+        JsonArray edges = (JsonArray) topology.get("edges");
+        assertThat(edges).hasSize(1);
+        JsonObject edge = (JsonObject) edges.get(0);
+        assertThat(edge.getString("fromRouteId")).isEqualTo("intake");
+        assertThat(edge.getString("toRouteId")).isEqualTo("billing");
+    }
+
+    @Test
+    void structureReadsJavaRoutesWithoutCompilingThem(@TempDir Path dir) 
throws Exception {
+        Path billing = write(dir, "Billing.java", BILLING);
+        StringPrinter printer = new StringPrinter();
+        CamelRouteStructureAction command = new CamelRouteStructureAction(new 
CamelJBangMain().withPrinter(printer));
+        command.name = billing.toString();
+
+        int exit = command.doCall();
+
+        assertThat(exit).isZero();
+        assertThat(printer.getOutput())
+                .contains("Source: Billing.java")
+                .contains("billing")
+                .contains("direct:billing")
+                .contains("an object created in the route");
+    }
+
+    private static Path write(Path dir, String name, String content) throws 
Exception {
+        Path file = dir.resolve(name);
+        Files.writeString(file, content);
+        return file;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
index 9d4e5a56491d..302f287edbfb 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
@@ -42,7 +42,9 @@ public class RouteDiagramTools {
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = "Generate a diagram of Camel routes from a route 
source file (YAML, XML, Java, ...). "
-                        + "The source file is parsed without running it. "
+                        + "Java routes are read by the Java DSL parser, 
without compiling them; a step it cannot "
+                        + "work out (a lambda, a processor) is shown as a 
placeholder, and the parts it could not read "
+                        + "are listed in the message. "
                         + "For image themes (dark, light, transparent) the PNG 
is written to disk and the absolute path "
                         + "is returned. For text themes (ascii, unicode) the 
diagram is returned directly as text so "
                         + "it can be read and understood by the caller. "
@@ -120,13 +122,13 @@ public class RouteDiagramTools {
             if (textMode) {
                 String asciiContent = success ? Files.readString(out.toPath()) 
: null;
                 String message = success
-                        ? "ASCII diagram generated (" + size + " bytes)"
+                        ? withNotes("ASCII diagram generated (" + size + " 
bytes)", capturingPrinter)
                         : "Failed to render diagram (exit code " + exit + "): 
" + capturingPrinter.getOutput();
                 return new RouteDiagramResult(success, resolvedOutput, size, 
message, asciiContent);
             }
 
             String message = success
-                    ? "Diagram saved to: " + resolvedOutput
+                    ? withNotes("Diagram saved to: " + resolvedOutput, 
capturingPrinter)
                     : "Failed to render diagram (exit code " + exit + "): " + 
capturingPrinter.getOutput();
             return new RouteDiagramResult(success, resolvedOutput, size, 
message, null);
         } catch (Throwable e) {
@@ -135,6 +137,14 @@ public class RouteDiagramTools {
         }
     }
 
+    /**
+     * The message, followed by what the command printed, such as the parts of 
a Java route the parser could not read.
+     */
+    private static String withNotes(String message, CapturingPrinter printer) {
+        String notes = printer.getOutput();
+        return notes.isEmpty() ? message : message + "\n" + notes;
+    }
+
     /**
      * @param asciiDiagram plain-text diagram content for ascii/unicode 
themes; null for image themes
      */

Reply via email to