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 e5d85f3ac0d9 CAMEL-25196: camel-jbang - TUI Source tab route jump 
links for XML DSL routes (#27144)
e5d85f3ac0d9 is described below

commit e5d85f3ac0d9183b16caeee430dbd3c052a71d69
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 30 22:09:29 2026 +0200

    CAMEL-25196: camel-jbang - TUI Source tab route jump links for XML DSL 
routes (#27144)
    
    …L routes
    
    The route index reads Camel XML files with a stream reader (no DTD, no
    external entities): the from of each route and what it sends to (to, toD,
    wireTap, enrich, pollEnrich, and the case and otherwise of a switch), with
    their lines, so XML routes link to and from the YAML and Java ones.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/JavaRouteScanner.java  |  24 ++--
 .../dsl/jbang/core/commands/tui/ScannedRoute.java  |  30 +++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  25 +++--
 .../jbang/core/commands/tui/XmlRouteScanner.java   | 124 +++++++++++++++++++++
 .../src/main/resources/tui/help/source.md          |   4 +-
 .../core/commands/tui/JavaRouteScannerTest.java    |  30 ++---
 .../core/commands/tui/SourceTabJumpLinksTest.java  |  31 ++++++
 .../core/commands/tui/XmlRouteScannerTest.java     |  79 +++++++++++++
 8 files changed, 307 insertions(+), 40 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScanner.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScanner.java
index c4569180ca64..4a0a5858fdff 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScanner.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScanner.java
@@ -48,14 +48,6 @@ final class JavaRouteScanner {
 
     private static final int MAX_DEPTH = 50;
 
-    /** A route: its id (null when it has none), its from endpoint and the 
line of from, from 0. */
-    record Route(String id, String fromUri, int line, List<To> tos) {
-    }
-
-    /** An endpoint a route sends to, and the line of the step, from 0. */
-    record To(String uri, int line) {
-    }
-
     private JavaRouteScanner() {
     }
 
@@ -69,24 +61,24 @@ final class JavaRouteScanner {
      *
      * @param javaSources the Java sources of the project by path, for the 
constants of other classes
      */
-    static List<Route> scan(String content, Map<String, Supplier<String>> 
javaSources, CamelCatalog catalog) {
-        List<Route> answer = new ArrayList<>();
+    static List<ScannedRoute> scan(String content, Map<String, 
Supplier<String>> javaSources, CamelCatalog catalog) {
+        List<ScannedRoute> answer = new ArrayList<>();
         for (RouteDefinition r : ProjectRoutes.parseJava(content, javaSources, 
catalog).routes().getRoutes()) {
             if (r.getInput() == null || r.getInput().getUri() == null) {
                 continue;
             }
             int line = r.getInput().getLineNumber() > 0 ? 
r.getInput().getLineNumber() : r.getLineNumber();
-            List<To> tos = new ArrayList<>();
+            List<ScannedRoute.To> tos = new ArrayList<>();
             Set<Object> seen = Collections.newSetFromMap(new 
IdentityHashMap<>());
             for (ProcessorDefinition<?> p : r.getOutputs()) {
                 walk(p, tos, seen, 0);
             }
-            answer.add(new Route(r.getRouteId(), uri(r.getInput().getUri()), 
Math.max(0, line - 1), tos));
+            answer.add(new ScannedRoute(r.getRouteId(), 
uri(r.getInput().getUri()), Math.max(0, line - 1), tos));
         }
         return answer;
     }
 
-    private static void walk(ProcessorDefinition<?> p, List<To> tos, 
Set<Object> seen, int depth) {
+    private static void walk(ProcessorDefinition<?> p, List<ScannedRoute.To> 
tos, Set<Object> seen, int depth) {
         if (p == null || depth > MAX_DEPTH || !seen.add(p)) {
             return;
         }
@@ -101,7 +93,7 @@ final class JavaRouteScanner {
             uri = c.getExpression();
         }
         if (uri != null && p.getLineNumber() > 0) {
-            tos.add(new To(uri(uri), p.getLineNumber() - 1));
+            tos.add(new ScannedRoute.To(uri(uri), p.getLineNumber() - 1));
         }
         if (p instanceof SwitchDefinition sw) {
             // the destinations of a switch are its cases and fallback, not 
outputs
@@ -123,9 +115,9 @@ final class JavaRouteScanner {
         }
     }
 
-    private static void add(List<To> tos, String uri, int line) {
+    private static void add(List<ScannedRoute.To> tos, String uri, int line) {
         if (uri != null && line > 0) {
-            tos.add(new To(uri(uri), line - 1));
+            tos.add(new ScannedRoute.To(uri(uri), line - 1));
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScannedRoute.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScannedRoute.java
new file mode 100644
index 000000000000..c90309c248ff
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ScannedRoute.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * A route as a source scanner of the Source tab reads it, for the jump links: 
its id (null when it has none), its from
+ * endpoint and the line of from, from 0, and the endpoints it sends to.
+ */
+record ScannedRoute(String id, String fromUri, int line, List<ScannedRoute.To> 
tos) {
+
+    /** An endpoint a route sends to, and the line of the step, from 0. */
+    record To(String uri, int line) {
+    }
+}
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 c097940799e6..1996afa2970e 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
@@ -1049,6 +1049,11 @@ class SourceTab extends AbstractTab {
             }
             if (SourceEditAssist.isYamlFile(path)) {
                 scanYamlRoutes(path, fromEntries, toEntries);
+            } else if 
(path.getFileName().toString().toLowerCase().endsWith(".xml")) {
+                String content = readQuietly(path);
+                if (content != null) {
+                    addRoutes(path, XmlRouteScanner.scan(content), 
fromEntries, toEntries);
+                }
             }
         }
         routeIndex = fromEntries;
@@ -1059,16 +1064,20 @@ class SourceTab extends AbstractTab {
     private void scanJavaRoutes(
             Path file, Map<String, Supplier<String>> javaSources, 
List<RouteEntry> fromEntries, List<ToEntry> toEntries) {
         String content = readQuietly(file);
-        if (content == null) {
-            return;
+        if (content != null) {
+            addRoutes(file, JavaRouteScanner.scan(content, javaSources, 
ArchitectureView.catalog()), fromEntries, toEntries);
         }
+    }
+
+    /** Adds the routes a scanner read from a file (Java or XML) to the index. 
*/
+    private void addRoutes(Path file, List<ScannedRoute> routes, 
List<RouteEntry> fromEntries, List<ToEntry> toEntries) {
         String filePath = file.toString();
-        for (JavaRouteScanner.Route route : JavaRouteScanner.scan(content, 
javaSources, ArchitectureView.catalog())) {
+        for (ScannedRoute route : routes) {
             int before = fromEntries.size();
-            emitRouteEntry(fromEntries, route.id(), route.fromUri(), filePath, 
route.line());
+            emitRouteEntry(fromEntries, route.id(), 
JavaRouteScanner.uri(route.fromUri()), filePath, route.line());
             String routeId = fromEntries.size() > before ? 
fromEntries.get(before).routeId() : "";
-            for (JavaRouteScanner.To to : route.tos()) {
-                String toUri = stripQueryParams(to.uri());
+            for (ScannedRoute.To to : route.tos()) {
+                String toUri = 
stripQueryParams(JavaRouteScanner.uri(to.uri()));
                 if (toUri != null && !toUri.isEmpty()) {
                     toEntries.add(new ToEntry(routeId, toUri, filePath, 
to.line()));
                 }
@@ -1244,8 +1253,8 @@ class SourceTab extends AbstractTab {
             fromUriToRoute.put(re.fromUri(), re);
         }
 
-        if (currentFilePath.endsWith(".java")) {
-            // the steps of a Java route are in the index with their lines, as 
the parser read them
+        if (currentFilePath.endsWith(".java") || 
currentFilePath.toLowerCase().endsWith(".xml")) {
+            // the steps of a Java or XML route are in the index with their 
lines, as the parser read them
             for (ToEntry te : toIndex) {
                 if (currentFilePath.equals(te.filePath())) {
                     RouteEntry target = fromUriToRoute.get(te.toUri());
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/XmlRouteScanner.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/XmlRouteScanner.java
new file mode 100644
index 000000000000..e1a7b59211a4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/XmlRouteScanner.java
@@ -0,0 +1,124 @@
+/*
+ * 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.io.StringReader;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.Set;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+/**
+ * The routes of a Camel XML source for the jump links of the Source tab 
(CAMEL-25196): each route's from endpoint and
+ * the endpoints it sends to, with their lines. The XML is read as a stream, 
without DTDs or external entities, and
+ * nothing of the project is loaded.
+ */
+final class XmlRouteScanner {
+
+    /** The steps whose uri attribute is an endpoint a route sends to. */
+    private static final Set<String> SENDING = Set.of("to", "toD", "wireTap", 
"enrich", "pollEnrich");
+
+    private XmlRouteScanner() {
+    }
+
+    /** The routes of an XML source; empty when it has none or is not 
well-formed. */
+    static List<ScannedRoute> scan(String content) {
+        List<ScannedRoute> answer = new ArrayList<>();
+        XMLInputFactory factory = XMLInputFactory.newFactory();
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, 
false);
+        XMLStreamReader xml = null;
+        try {
+            xml = factory.createXMLStreamReader(new StringReader(content));
+            Deque<String> open = new ArrayDeque<>();
+            String id = null;
+            String from = null;
+            int fromLine = 0;
+            List<ScannedRoute.To> tos = null;
+            // an enrich or pollEnrich whose endpoint is a <constant> below 
it, and its line
+            int enrichLine = -1;
+            while (xml.hasNext()) {
+                int event = xml.next();
+                if (event == XMLStreamConstants.START_ELEMENT) {
+                    String name = xml.getLocalName();
+                    int line = Math.max(0, xml.getLocation().getLineNumber() - 
1);
+                    String parent = open.peek();
+                    open.push(name);
+                    if ("route".equals(name)) {
+                        id = xml.getAttributeValue(null, "id");
+                        from = null;
+                        tos = new ArrayList<>();
+                        enrichLine = -1;
+                    } else if (tos == null) {
+                        continue;
+                    } else if ("from".equals(name) && from == null) {
+                        from = xml.getAttributeValue(null, "uri");
+                        fromLine = line;
+                    } else if (SENDING.contains(name)) {
+                        String uri = xml.getAttributeValue(null, "uri");
+                        if (uri != null) {
+                            tos.add(new ScannedRoute.To(uri, line));
+                        } else if ("enrich".equals(name) || 
"pollEnrich".equals(name)) {
+                            enrichLine = line;
+                        }
+                    } else if (("case".equals(name) || 
"otherwise".equals(name)) && "switch".equals(parent)) {
+                        // the destinations of a switch
+                        String uri = xml.getAttributeValue(null, "uri");
+                        if (uri != null) {
+                            tos.add(new ScannedRoute.To(uri, line));
+                        }
+                    } else if ("constant".equals(name) && enrichLine >= 0
+                            && ("enrich".equals(parent) || 
"pollEnrich".equals(parent))) {
+                        String uri = xml.getElementText().strip();
+                        open.pop();
+                        if (!uri.isEmpty()) {
+                            tos.add(new ScannedRoute.To(uri, enrichLine));
+                        }
+                        enrichLine = -1;
+                    }
+                } else if (event == XMLStreamConstants.END_ELEMENT) {
+                    String name = open.isEmpty() ? null : open.pop();
+                    if ("route".equals(name) && tos != null) {
+                        if (from != null && !from.isBlank()) {
+                            answer.add(new ScannedRoute(id, from.strip(), 
fromLine, tos));
+                        }
+                        tos = null;
+                    } else if ("enrich".equals(name) || 
"pollEnrich".equals(name)) {
+                        enrichLine = -1;
+                    }
+                }
+            }
+        } catch (XMLStreamException | RuntimeException e) {
+            // a file being edited may not be well-formed: what was read so 
far is kept
+        } finally {
+            if (xml != null) {
+                try {
+                    xml.close();
+                } catch (XMLStreamException e) {
+                    // ignore
+                }
+            }
+        }
+        return answer;
+    }
+}
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 73708455ab23..5766650940c2 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
@@ -74,7 +74,9 @@ Lines with `to:`, `toD:`, `wireTap:`, or similar endpoints 
that reference
 another route show a **↵ routeId** indicator. Press **Enter** on such a line
 to jump to the target route's definition (within the same file or across 
files).
 Reverse links are shown on `from:` lines, indicating which route calls this 
one.
-Jump indicators are hidden in plain mode.
+This works for YAML, XML and Java DSL routes, also across files of different
+DSLs; Java routes are read without compiling them. The case and otherwise of a
+switch link like a `to`. Jump indicators are hidden in plain mode.
 
 ## Go to Route
 - **g** — open a filterable popup listing all routes found in the source files.
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScannerTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScannerTest.java
index 5fb57ba8389c..d9f3dff25be9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScannerTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScannerTest.java
@@ -65,24 +65,24 @@ class JavaRouteScannerTest {
     @Test
     void routesAndTheirStepsWithLines() {
         Map<String, Supplier<String>> sources = Map.of("/p/Orders.java", () -> 
ORDERS, "/p/Endpoints.java", () -> ENDPOINTS);
-        List<JavaRouteScanner.Route> routes = JavaRouteScanner.scan(ORDERS, 
sources, new DefaultCamelCatalog());
+        List<ScannedRoute> routes = JavaRouteScanner.scan(ORDERS, sources, new 
DefaultCamelCatalog());
 
-        
assertThat(routes).extracting(JavaRouteScanner.Route::id).containsExactly("intake",
 null);
-        JavaRouteScanner.Route intake = routes.get(0);
+        
assertThat(routes).extracting(ScannedRoute::id).containsExactly("intake", null);
+        ScannedRoute intake = routes.get(0);
         assertThat(intake.fromUri()).isEqualTo("platform-http:/orders");
         assertThat(intake.line()).isEqualTo(7);
         // lines from 0, as the Source tab counts them; the constant of the 
other class is resolved
         assertThat(intake.tos()).containsExactly(
-                new JavaRouteScanner.To("direct:vip", 10),
-                new JavaRouteScanner.To("direct:standard", 12),
-                new JavaRouteScanner.To("direct:audit?block=false", 14));
+                new ScannedRoute.To("direct:vip", 10),
+                new ScannedRoute.To("direct:standard", 12),
+                new ScannedRoute.To("direct:audit?block=false", 14));
 
-        JavaRouteScanner.Route vip = routes.get(1);
+        ScannedRoute vip = routes.get(1);
         assertThat(vip.fromUri()).isEqualTo("direct:vip");
         assertThat(vip.line()).isEqualTo(16);
         assertThat(vip.tos()).containsExactly(
-                new JavaRouteScanner.To("kafka:vip-${header.region}", 18),
-                new JavaRouteScanner.To("direct:audit", 20));
+                new ScannedRoute.To("kafka:vip-${header.region}", 18),
+                new ScannedRoute.To("direct:audit", 20));
     }
 
     @Test
@@ -94,10 +94,10 @@ class JavaRouteScannerTest {
                     }
                 }
                 """;
-        List<JavaRouteScanner.Route> routes = JavaRouteScanner.scan(source, 
Map.of(), new DefaultCamelCatalog());
+        List<ScannedRoute> routes = JavaRouteScanner.scan(source, Map.of(), 
new DefaultCamelCatalog());
         // direct://in as a route written as text has it, so it links to 
from("direct:in") in another file
         assertThat(routes.get(0).fromUri()).isEqualTo("direct:in");
-        assertThat(routes.get(0).tos()).containsExactly(new 
JavaRouteScanner.To("direct:out", 2));
+        assertThat(routes.get(0).tos()).containsExactly(new 
ScannedRoute.To("direct:out", 2));
     }
 
     @Test
@@ -114,11 +114,11 @@ class JavaRouteScannerTest {
                     }
                 }
                 """;
-        List<JavaRouteScanner.Route> routes = JavaRouteScanner.scan(source, 
Map.of(), new DefaultCamelCatalog());
+        List<ScannedRoute> routes = JavaRouteScanner.scan(source, Map.of(), 
new DefaultCamelCatalog());
         assertThat(routes.get(0).tos()).containsExactly(
-                new JavaRouteScanner.To("direct:billing", 4),
-                new JavaRouteScanner.To("direct:technical", 5),
-                new JavaRouteScanner.To("direct:review", 6));
+                new ScannedRoute.To("direct:billing", 4),
+                new ScannedRoute.To("direct:technical", 5),
+                new ScannedRoute.To("direct:review", 6));
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
index bae5283c492f..8effc58fae12 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
@@ -128,6 +128,37 @@ class SourceTabJumpLinksTest {
         assertThat(back.routeId()).isEqualTo("file:inbox");
     }
 
+    @Test
+    void xmlRoutesLinkToAndFromTheOthers() throws Exception {
+        // CAMEL-25196: XML routes have the jump links of the YAML and Java 
ones
+        Path orders = Files.writeString(dir.resolve("orders.xml"), """
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                  <route id="orders">
+                    <from uri="file:inbox"/>
+                    <to uri="direct:billing"/>
+                  </route>
+                </routes>
+                """);
+        Path billing = Files.writeString(dir.resolve("billing.camel.yaml"), """
+                - route:
+                    id: billing
+                    from:
+                      uri: direct:billing
+                      steps:
+                        - to:
+                            uri: log:billing
+                """);
+        SourceTab tab = newTab();
+        assertThat(tab.loadDirectory(dir)).isTrue();
+
+        SourceViewer.JumpLink link = tab.computeJumpLinks(orders).get(3);
+        assertThat(link.routeId()).isEqualTo("billing");
+        assertThat(link.filePath()).isEqualTo(billing.toString());
+        SourceViewer.JumpLink back = tab.computeJumpLinks(billing).get(2);
+        assertThat(back.routeId()).isEqualTo("orders");
+        assertThat(back.targetLine()).isEqualTo(3);
+    }
+
     private static SourceTab newTab() {
         return new SourceTab(new MonitorContext(new 
AtomicReference<>(List.of()), new AtomicReference<>(List.of())));
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/XmlRouteScannerTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/XmlRouteScannerTest.java
new file mode 100644
index 000000000000..806e3aca9eae
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/XmlRouteScannerTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class XmlRouteScannerTest {
+
+    @Test
+    void routesAndTheirStepsWithLines() {
+        List<ScannedRoute> routes = XmlRouteScanner.scan("""
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                  <route id="tickets">
+                    <from uri="direct:tickets"/>
+                    <choice>
+                      <when>
+                        <simple>${header.vip}</simple>
+                        <to uri="direct:vip"/>
+                      </when>
+                    </choice>
+                    <switch>
+                      <selector><header>department</header></selector>
+                      <case value="billing" uri="direct:billing"/>
+                      <otherwise uri="direct:review"/>
+                    </switch>
+                    <enrich>
+                      <constant>direct:customer</constant>
+                    </enrich>
+                    <wireTap uri="seda:audit?block=false"/>
+                  </route>
+                  <route>
+                    <from uri="direct:vip"/>
+                    <toD uri="kafka:vip-${header.region}"/>
+                  </route>
+                </routes>
+                """);
+        
assertThat(routes).extracting(ScannedRoute::id).containsExactly("tickets", 
null);
+        ScannedRoute tickets = routes.get(0);
+        assertThat(tickets.fromUri()).isEqualTo("direct:tickets");
+        assertThat(tickets.line()).isEqualTo(2);
+        // lines from 0, as the Source tab counts them
+        assertThat(tickets.tos()).containsExactly(
+                new ScannedRoute.To("direct:vip", 6),
+                new ScannedRoute.To("direct:billing", 11),
+                new ScannedRoute.To("direct:review", 12),
+                new ScannedRoute.To("direct:customer", 14),
+                new ScannedRoute.To("seda:audit?block=false", 17));
+        assertThat(routes.get(1).tos()).containsExactly(new 
ScannedRoute.To("kafka:vip-${header.region}", 21));
+    }
+
+    @Test
+    void notWellFormedOrWithoutRoutes() {
+        assertThat(XmlRouteScanner.scan("<routes><route><from 
uri=\"direct:a\"/>")).isEmpty();
+        assertThat(XmlRouteScanner.scan("<beans/>")).isEmpty();
+        // no DTD, no external entities
+        assertThat(XmlRouteScanner.scan("""
+                <!DOCTYPE routes [<!ENTITY x SYSTEM "file:///etc/passwd">]>
+                <routes><route><from uri="direct:&x;"/></route></routes>
+                """)).allSatisfy(r -> 
assertThat(r.fromUri()).doesNotContain("root"));
+    }
+}

Reply via email to