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

gnodet pushed a commit to branch calm-hisser
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e8d2a616dd378687d99e25ea96a53177eaaf4374
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Mar 23 22:11:37 2026 +0100

    CAMEL-22544: jbang dependency update supports multi-file and --scan-routes
    
    - Accept multiple target files (pom.xml or Java source files with //DEPS)
    - Add --scan-routes flag to sync dependencies from route definitions:
      - Manages only org.apache.camel dependencies
      - Preserves non-Camel dependencies
      - Removes unused Camel dependencies
      - Idempotent on re-execution
    - Route files (YAML, XML) passed as arguments are used as source files
      for the export pipeline dependency resolution
    - In scan-routes mode, Camel //DEPS in Java target files are stripped
      before export to prevent stale deps from being resolved
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/DependencyUpdate.java  | 506 ++++++++++++++++++---
 .../core/commands/DependencyUpdateJBangTest.java   | 276 +++++++++++
 2 files changed, 715 insertions(+), 67 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdate.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdate.java
index 5c96cc3fbc5d..59efc5b1da80 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdate.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdate.java
@@ -20,7 +20,9 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.StringJoiner;
 
 import org.w3c.dom.Document;
@@ -38,21 +40,30 @@ import org.apache.logging.log4j.util.Strings;
 import picocli.CommandLine;
 
 @CommandLine.Command(name = "update",
-                     description = "Updates dependencies in Maven pom.xml or 
Java source file (JBang style)",
+                     description = "Updates dependencies in Maven pom.xml or 
Java source files (JBang style)",
                      sortOptions = false,
                      showDefaultValues = true)
 public class DependencyUpdate extends DependencyList {
 
-    @CommandLine.Parameters(description = "Maven pom.xml or Java source files 
(JBang Style with //DEPS) to have dependencies updated",
-                            arity = "1")
-    public Path file;
+    @CommandLine.Parameters(description = "Maven pom.xml or Java source files 
(JBang Style with //DEPS) to have dependencies updated."
+                                          + " Route definition files (YAML, 
XML) can also be included and will be used as source files"
+                                          + " for dependency resolution.",
+                            arity = "1..*")
+    public List<Path> targetFiles;
 
     @CommandLine.Option(names = { "--clean" },
                         description = "Regenerate list of dependencies (do not 
keep existing dependencies). Not supported for pom.xml")
     protected boolean clean;
 
+    @CommandLine.Option(names = { "--scan-routes" },
+                        description = "Sync dependencies from route 
definitions. Only manages org.apache.camel dependencies,"
+                                      + " preserving non-Camel dependencies. 
Removes unused Camel dependencies.")
+    protected boolean scanRoutes;
+
     private final List<String> deps = new ArrayList<>();
     private final List<MavenGav> gavs = new ArrayList<>();
+    // actual target files to update (pom.xml or Java files with //DEPS)
+    private final List<Path> updateTargets = new ArrayList<>();
 
     public DependencyUpdate(CamelJBangMain main) {
         super(main);
@@ -60,22 +71,53 @@ public class DependencyUpdate extends DependencyList {
 
     @Override
     public Integer doCall() throws Exception {
-        // source file must exist
-        if (!Files.exists(file)) {
-            printer().printErr("Source file does not exist: " + file);
+        // classify target files: pom.xml and Java files are update targets,
+        // route definition files (yaml, xml, etc.) are source files for the 
export pipeline
+        updateTargets.clear();
+        for (Path file : targetFiles) {
+            if (!Files.exists(file)) {
+                printer().printErr("Source file does not exist: " + file);
+                return -1;
+            }
+            String name = file.getFileName().toString();
+            String ext = FileUtil.onlyExt(name, true);
+            if ("pom.xml".equals(name) || "java".equals(ext)) {
+                updateTargets.add(file);
+            }
+            // route source files are already added to this.files by the 
parent's parameter consumer
+        }
+
+        if (scanRoutes) {
+            // in scan-routes mode, strip existing //DEPS from Java target 
files before the export pipeline
+            // runs, so only actual route-based dependencies are resolved (not 
stale //DEPS)
+            for (Path file : updateTargets) {
+                String ext = FileUtil.onlyExt(file.getFileName().toString(), 
true);
+                if ("java".equals(ext)) {
+                    stripJBangDeps(file);
+                }
+            }
+        }
+
+        if (updateTargets.isEmpty()) {
+            printer().printErr("No target files (pom.xml or Java source files) 
specified");
             return -1;
         }
 
-        boolean maven = "pom.xml".equals(file.getFileName().toString());
+        Path firstTarget = updateTargets.get(0);
+        boolean maven = "pom.xml".equals(firstTarget.getFileName().toString());
 
-        if (clean && !maven) {
-            // remove DEPS in source file first
-            updateJBangSource();
+        if ((clean || scanRoutes) && !maven) {
+            // in clean mode: remove all DEPS first
+            for (Path file : updateTargets) {
+                if (clean) {
+                    updateJBangSource(file);
+                }
+            }
         }
 
         if (maven && this.runtime == null) {
             // Basic heuristic to determine if the project is a Quarkus or 
Spring Boot one.
-            String pomContent = new String(Files.readAllBytes(file));
+            String pomContent = new String(Files.readAllBytes(firstTarget));
             if (pomContent.contains("quarkus")) {
                 runtime = RuntimeType.quarkus;
             } else if (pomContent.contains("spring-boot")) {
@@ -93,7 +135,8 @@ public class DependencyUpdate extends DependencyList {
     @Override
     protected void outputGav(MavenGav gav, int index, int total) {
         try {
-            boolean maven = "pom.xml".equals(file.getFileName().toString());
+            Path firstTarget = updateTargets.get(0);
+            boolean maven = 
"pom.xml".equals(firstTarget.getFileName().toString());
             if (maven) {
                 outputGavMaven(gav, index, total);
             } else {
@@ -109,7 +152,9 @@ public class DependencyUpdate extends DependencyList {
 
         boolean last = total - index <= 1;
         if (last) {
-            updateMavenSource();
+            for (Path file : updateTargets) {
+                updateMavenSource(file);
+            }
         }
     }
 
@@ -127,11 +172,17 @@ public class DependencyUpdate extends DependencyList {
         }
         boolean last = total - index <= 1;
         if (last) {
-            updateJBangSource();
+            for (Path file : updateTargets) {
+                if (scanRoutes) {
+                    syncJBangSource(file);
+                } else {
+                    updateJBangSource(file);
+                }
+            }
         }
     }
 
-    private void updateJBangSource() {
+    private void updateJBangSource(Path file) {
         try {
             List<String> lines = Files.readAllLines(file);
             List<String> answer = new ArrayList<>();
@@ -155,7 +206,7 @@ public class DependencyUpdate extends DependencyList {
             }
             // add after shebang in top
             if (pos == -1) {
-                if (answer.get(0).trim().startsWith("///usr/bin/env jbang")) {
+                if (!answer.isEmpty() && 
answer.get(0).trim().startsWith("///usr/bin/env jbang")) {
                     pos = 1;
                 }
             }
@@ -164,8 +215,9 @@ public class DependencyUpdate extends DependencyList {
             }
 
             // reverse collection as we insert pos based
-            Collections.reverse(deps);
-            for (String dep : deps) {
+            List<String> depsToInsert = new ArrayList<>(deps);
+            Collections.reverse(depsToInsert);
+            for (String dep : depsToInsert) {
                 // is this XML or YAML
                 String ext = FileUtil.onlyExt(file.getFileName().toString(), 
true);
                 if ("yaml".equals(ext)) {
@@ -181,7 +233,124 @@ public class DependencyUpdate extends DependencyList {
         }
     }
 
-    private void updateMavenSource() throws Exception {
+    /**
+     * Syncs JBang source file dependencies: preserves non-Camel //DEPS, 
replaces Camel //DEPS with the resolved set.
+     */
+    private void syncJBangSource(Path file) {
+        try {
+            List<String> lines = Files.readAllLines(file);
+            List<String> answer = new ArrayList<>();
+
+            // collect non-Camel DEPS and find insertion position
+            List<String> nonCamelDeps = new ArrayList<>();
+            int pos = -1;
+            for (int i = 0; i < lines.size(); i++) {
+                String o = lines.get(i);
+                // remove leading comment chars to inspect
+                String l = o.trim();
+                while (l.startsWith("#")) {
+                    l = l.substring(1);
+                }
+                if (l.startsWith("//DEPS ")) {
+                    if (pos == -1) {
+                        pos = i;
+                    }
+                    // check if this is a Camel dependency
+                    String depPart = l.substring("//DEPS ".length()).trim();
+                    if (!isCamelDependency(depPart)) {
+                        nonCamelDeps.add(o);
+                    }
+                } else {
+                    answer.add(o);
+                }
+            }
+            // add after shebang in top
+            if (pos == -1) {
+                if (!answer.isEmpty() && 
answer.get(0).trim().startsWith("///usr/bin/env jbang")) {
+                    pos = 1;
+                }
+            }
+            if (pos == -1) {
+                pos = 0;
+            }
+
+            // build combined deps: Camel deps (from resolved) + non-Camel 
deps (preserved)
+            Set<String> seen = new LinkedHashSet<>();
+            List<String> allDeps = new ArrayList<>();
+
+            // add resolved Camel deps first
+            for (String dep : deps) {
+                if (seen.add(dep)) {
+                    allDeps.add(dep);
+                }
+            }
+            // add preserved non-Camel deps
+            for (String dep : nonCamelDeps) {
+                if (seen.add(dep)) {
+                    allDeps.add(dep);
+                }
+            }
+
+            // reverse collection as we insert pos based
+            Collections.reverse(allDeps);
+            for (String dep : allDeps) {
+                // is this XML or YAML
+                String ext = FileUtil.onlyExt(file.getFileName().toString(), 
true);
+                if ("yaml".equals(ext) && !dep.startsWith("#")) {
+                    dep = "#" + dep;
+                }
+                answer.add(pos, dep);
+            }
+
+            String text = String.join(System.lineSeparator(), answer);
+            Files.writeString(file, text);
+        } catch (Exception e) {
+            printer().printErr("Error updating source file: " + file + " due 
to: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Strips Camel //DEPS lines from a JBang-style source file (in-place), 
preserving non-Camel //DEPS. This is used in
+     * scan-routes mode to prevent stale Camel //DEPS from being picked up by 
the export pipeline.
+     */
+    private void stripJBangDeps(Path file) {
+        try {
+            List<String> lines = Files.readAllLines(file);
+            List<String> answer = new ArrayList<>();
+            for (String line : lines) {
+                String l = line.trim();
+                while (l.startsWith("#")) {
+                    l = l.substring(1);
+                }
+                if (l.startsWith("//DEPS ")) {
+                    String depPart = l.substring("//DEPS ".length()).trim();
+                    if (isCamelDependency(depPart)) {
+                        // skip Camel deps — they will be re-added from route 
resolution
+                        continue;
+                    }
+                }
+                answer.add(line);
+            }
+            String text = String.join(System.lineSeparator(), answer);
+            Files.writeString(file, text);
+        } catch (Exception e) {
+            printer().printErr("Error stripping //DEPS from: " + file + " due 
to: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Check if a dependency GAV string is a Camel dependency 
(org.apache.camel group).
+     */
+    private static boolean isCamelDependency(String dep) {
+        // handle @pom suffix
+        String d = dep;
+        if (d.endsWith("@pom")) {
+            d = d.substring(0, d.length() - 4);
+        }
+        return d.startsWith("org.apache.camel:");
+    }
+
+    private void updateMavenSource(Path file) throws Exception {
         List<MavenGav> existingGavs = new ArrayList<>();
 
         Node camelClone = null;
@@ -270,62 +439,265 @@ public class DependencyUpdate extends DependencyList {
             }
             // sort the new JARs being added
             updates.sort(mavenGavComparator());
-            List<MavenGav> toBeUpdated = new ArrayList<>();
-            int changes = 0;
-            for (MavenGav update : updates) {
-                if (!existingGavs.contains(update)) {
-                    toBeUpdated.add(update);
-                    changes++;
+
+            if (scanRoutes) {
+                // in scan-routes mode, sync Camel deps: add missing, remove 
unused
+                syncMavenSource(file, dom, existingGavs, updates);
+            } else {
+                // default mode: only add new deps
+                addMavenDeps(file, existingGavs, updates, targetLineNumber);
+            }
+        } else {
+            outPrinter().println("pom.xml not found " + pom.toAbsolutePath());
+        }
+    }
+
+    private void addMavenDeps(Path file, List<MavenGav> existingGavs, 
List<MavenGav> updates, int targetLineNumber)
+            throws Exception {
+        List<MavenGav> toBeUpdated = new ArrayList<>();
+        int changes = 0;
+        for (MavenGav update : updates) {
+            if (!existingGavs.contains(update)) {
+                toBeUpdated.add(update);
+                changes++;
+            }
+        }
+
+        if (changes > 0) {
+            writeMavenDeps(file, toBeUpdated, targetLineNumber);
+            if (changes > 1) {
+                outPrinter().println("Updating pom.xml with " + changes + " 
dependencies added");
+            } else {
+                outPrinter().println("Updating pom.xml with 1 dependency 
added");
+            }
+        } else {
+            outPrinter().println("No updates to pom.xml");
+        }
+    }
+
+    private void syncMavenSource(
+            Path file, Document dom, List<MavenGav> existingGavs, 
List<MavenGav> resolvedGavs)
+            throws Exception {
+
+        // determine which existing Camel deps are no longer needed
+        Set<String> resolvedGAs = new LinkedHashSet<>();
+        for (MavenGav gav : resolvedGavs) {
+            resolvedGAs.add(gav.getGroupId() + ":" + gav.getArtifactId());
+        }
+        Set<String> existingGAs = new LinkedHashSet<>();
+        for (MavenGav gav : existingGavs) {
+            existingGAs.add(gav.getGroupId() + ":" + gav.getArtifactId());
+        }
+
+        // find Camel deps to remove (exist in pom but not in resolved set)
+        List<String> toRemove = new ArrayList<>();
+        for (MavenGav gav : existingGavs) {
+            String ga = gav.getGroupId() + ":" + gav.getArtifactId();
+            boolean isCamel = gav.getGroupId().startsWith("org.apache.camel");
+            if (isCamel && !resolvedGAs.contains(ga)) {
+                // skip BOM entries
+                if (!"camel-bom".equals(gav.getArtifactId())
+                        && 
!"camel-spring-boot-bom".equals(gav.getArtifactId())) {
+                    toRemove.add(ga);
                 }
             }
+        }
 
-            if (changes > 0) {
-                // respect indent from existing GAVs
-                String line = 
IOHelper.loadTextLine(Files.newInputStream(file), targetLineNumber);
-                line = StringHelper.before(line, "<");
-                int indent = StringHelper.countChar(line, ' ');
-                String pad = Strings.repeat(" ", indent);
-                line = IOHelper.loadTextLine(Files.newInputStream(file), 
targetLineNumber - 1);
-                line = StringHelper.before(line, "<");
-                int indent2 = StringHelper.countChar(line, ' ');
-                String pad2 = Strings.repeat(" ", indent2);
-
-                // build GAVs to be added to pom.xml
-                StringJoiner sj = new StringJoiner("");
-                for (MavenGav gav : toBeUpdated) {
-                    sj.add("\n").add(pad).add("<dependency>\n");
-                    sj.add(pad2).add("<groupId>" + gav.getGroupId() + 
"</groupId>\n");
-                    sj.add(pad2).add("<artifactId>" + gav.getArtifactId() + 
"</artifactId>\n");
-                    if (gav.getVersion() != null) {
-                        sj.add(pad2).add("<version>" + gav.getVersion() + 
"</version>\n");
-                    }
-                    if (gav.getScope() != null) {
-                        sj.add(pad2).add("<scope>" + gav.getScope() + 
"</scope>\n");
-                    }
-                    sj.add(pad).add("</dependency>");
+        // find Camel deps to add (in resolved but not in pom)
+        List<MavenGav> toAdd = new ArrayList<>();
+        for (MavenGav gav : resolvedGavs) {
+            String ga = gav.getGroupId() + ":" + gav.getArtifactId();
+            if (!existingGAs.contains(ga)) {
+                toAdd.add(gav);
+            }
+        }
+
+        int added = toAdd.size();
+        int removed = toRemove.size();
+
+        if (added == 0 && removed == 0) {
+            outPrinter().println("No updates to pom.xml");
+            return;
+        }
+
+        // process file: remove unused Camel deps and add new ones
+        String content = IOHelper.loadText(Files.newInputStream(file));
+        String[] lines = content.split("\n");
+
+        if (removed > 0) {
+            content = removeMavenDeps(lines, dom, toRemove);
+        }
+
+        if (added > 0) {
+            // re-parse to get updated line numbers after removals
+            if (removed > 0) {
+                lines = content.split("\n");
+                Document updatedDom = XmlLineNumberParser.parseXml(
+                        new 
java.io.ByteArrayInputStream(content.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
+                int insertLine = findLastCamelDependencyLine(updatedDom);
+                if (insertLine > 0) {
+                    content = insertMavenDeps(lines, toAdd, insertLine);
+                }
+            } else {
+                int insertLine = findLastCamelDependencyLine(dom);
+                if (insertLine > 0) {
+                    content = insertMavenDeps(lines, toAdd, insertLine);
+                }
+            }
+        }
+
+        Files.writeString(file, content);
+
+        StringBuilder msg = new StringBuilder("Updating pom.xml with ");
+        if (added > 0) {
+            msg.append(added).append(added == 1 ? " dependency added" : " 
dependencies added");
+        }
+        if (removed > 0) {
+            if (added > 0) {
+                msg.append(" and ");
+            }
+            msg.append(removed).append(removed == 1 ? " dependency removed" : 
" dependencies removed");
+        }
+        outPrinter().println(msg.toString());
+    }
+
+    private int findLastCamelDependencyLine(Document dom) {
+        int targetLineNumber = -1;
+        NodeList nl = dom.getElementsByTagName("dependency");
+        for (int i = 0; i < nl.getLength(); i++) {
+            Element node = (Element) nl.item(i);
+            String p = node.getParentNode().getNodeName();
+            String p2 = node.getParentNode().getParentNode().getNodeName();
+            boolean accept = ("dependencyManagement".equals(p2) || 
"project".equals(p2)) && (p.equals("dependencies"));
+            if (!accept) {
+                continue;
+            }
+            String g = 
node.getElementsByTagName("groupId").item(0).getTextContent();
+            if (g.startsWith("org.apache.camel")) {
+                String num = (String) 
node.getUserData(XmlLineNumberParser.LINE_NUMBER_END);
+                if (num != null) {
+                    targetLineNumber = Integer.parseInt(num);
                 }
+            }
+        }
+        return targetLineNumber;
+    }
 
-                StringJoiner out = new StringJoiner("\n");
-                String[] lines = 
IOHelper.loadText(Files.newInputStream(file)).split("\n");
-                for (int i = 0; i < lines.length; i++) {
-                    String txt = lines[i];
-                    out.add(txt);
-                    if (i == targetLineNumber - 1) {
-                        out.add(sj.toString());
-                    }
+    private String removeMavenDeps(String[] lines, Document dom, List<String> 
toRemove) {
+        // find line ranges of dependencies to remove
+        List<int[]> rangesToRemove = new ArrayList<>();
+        NodeList nl = dom.getElementsByTagName("dependency");
+        for (int i = 0; i < nl.getLength(); i++) {
+            Element node = (Element) nl.item(i);
+            String p = node.getParentNode().getNodeName();
+            String p2 = node.getParentNode().getParentNode().getNodeName();
+            boolean accept = ("dependencyManagement".equals(p2) || 
"project".equals(p2)) && (p.equals("dependencies"));
+            if (!accept) {
+                continue;
+            }
+            String g = 
node.getElementsByTagName("groupId").item(0).getTextContent();
+            String a = 
node.getElementsByTagName("artifactId").item(0).getTextContent();
+            String ga = g + ":" + a;
+            if (toRemove.contains(ga)) {
+                String startNum = (String) 
node.getUserData(XmlLineNumberParser.LINE_NUMBER);
+                String endNum = (String) 
node.getUserData(XmlLineNumberParser.LINE_NUMBER_END);
+                if (startNum != null && endNum != null) {
+                    rangesToRemove.add(new int[] { Integer.parseInt(startNum), 
Integer.parseInt(endNum) });
                 }
-                if (changes > 1) {
-                    outPrinter().println("Updating pom.xml with " + changes + 
" dependencies added");
-                } else {
-                    outPrinter().println("Updating pom.xml with 1 dependency 
added");
+            }
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < lines.length; i++) {
+            int lineNum = i + 1; // 1-based
+            boolean skip = false;
+            for (int[] range : rangesToRemove) {
+                if (lineNum >= range[0] && lineNum <= range[1]) {
+                    skip = true;
+                    break;
                 }
-                Files.writeString(file, out.toString());
-            } else {
-                outPrinter().println("No updates to pom.xml");
             }
-        } else {
-            outPrinter().println("pom.xml not found " + pom.toAbsolutePath());
+            if (!skip) {
+                if (sb.length() > 0) {
+                    sb.append("\n");
+                }
+                sb.append(lines[i]);
+            }
+        }
+        return sb.toString();
+    }
+
+    private String insertMavenDeps(String[] lines, List<MavenGav> toAdd, int 
targetLineNumber) {
+        // respect indent from existing lines
+        String line = lines[targetLineNumber - 1];
+        String beforeTag = StringHelper.before(line, "<");
+        int indent = beforeTag != null ? StringHelper.countChar(beforeTag, ' 
') : 8;
+        String pad = Strings.repeat(" ", indent);
+        String line2 = targetLineNumber >= 2 ? lines[targetLineNumber - 2] : 
line;
+        String beforeTag2 = StringHelper.before(line2, "<");
+        int indent2 = beforeTag2 != null ? StringHelper.countChar(beforeTag2, 
' ') : indent + 4;
+        String pad2 = Strings.repeat(" ", indent2);
+
+        StringJoiner sj = new StringJoiner("");
+        for (MavenGav gav : toAdd) {
+            sj.add("\n").add(pad).add("<dependency>\n");
+            sj.add(pad2).add("<groupId>" + gav.getGroupId() + "</groupId>\n");
+            sj.add(pad2).add("<artifactId>" + gav.getArtifactId() + 
"</artifactId>\n");
+            if (gav.getVersion() != null) {
+                sj.add(pad2).add("<version>" + gav.getVersion() + 
"</version>\n");
+            }
+            if (gav.getScope() != null) {
+                sj.add(pad2).add("<scope>" + gav.getScope() + "</scope>\n");
+            }
+            sj.add(pad).add("</dependency>");
+        }
+
+        StringJoiner out = new StringJoiner("\n");
+        for (int i = 0; i < lines.length; i++) {
+            out.add(lines[i]);
+            if (i == targetLineNumber - 1) {
+                out.add(sj.toString());
+            }
+        }
+        return out.toString();
+    }
+
+    private void writeMavenDeps(Path file, List<MavenGav> toBeUpdated, int 
targetLineNumber) throws Exception {
+        // respect indent from existing GAVs
+        String line = IOHelper.loadTextLine(Files.newInputStream(file), 
targetLineNumber);
+        line = StringHelper.before(line, "<");
+        int indent = StringHelper.countChar(line, ' ');
+        String pad = Strings.repeat(" ", indent);
+        line = IOHelper.loadTextLine(Files.newInputStream(file), 
targetLineNumber - 1);
+        line = StringHelper.before(line, "<");
+        int indent2 = StringHelper.countChar(line, ' ');
+        String pad2 = Strings.repeat(" ", indent2);
+
+        // build GAVs to be added to pom.xml
+        StringJoiner sj = new StringJoiner("");
+        for (MavenGav gav : toBeUpdated) {
+            sj.add("\n").add(pad).add("<dependency>\n");
+            sj.add(pad2).add("<groupId>" + gav.getGroupId() + "</groupId>\n");
+            sj.add(pad2).add("<artifactId>" + gav.getArtifactId() + 
"</artifactId>\n");
+            if (gav.getVersion() != null) {
+                sj.add(pad2).add("<version>" + gav.getVersion() + 
"</version>\n");
+            }
+            if (gav.getScope() != null) {
+                sj.add(pad2).add("<scope>" + gav.getScope() + "</scope>\n");
+            }
+            sj.add(pad).add("</dependency>");
+        }
+
+        StringJoiner out = new StringJoiner("\n");
+        String[] lines = 
IOHelper.loadText(Files.newInputStream(file)).split("\n");
+        for (int i = 0; i < lines.length; i++) {
+            String txt = lines[i];
+            out.add(txt);
+            if (i == targetLineNumber - 1) {
+                out.add(sj.toString());
+            }
         }
+        Files.writeString(file, out.toString());
     }
 
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateJBangTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateJBangTest.java
new file mode 100644
index 000000000000..18305836f429
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateJBangTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.common.RuntimeType;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.apache.camel.util.FileUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DependencyUpdateJBangTest extends CamelCommandBaseTestSupport {
+
+    private File workingDir;
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        super.setup();
+        Path base = Paths.get("target");
+        workingDir = Files.createTempDirectory(base, 
"camel-dependency-update-jbang-tests").toFile();
+    }
+
+    @AfterEach
+    void end() {
+        FileUtil.removeDir(workingDir);
+    }
+
+    @ParameterizedTest
+    @MethodSource("runtimeProvider")
+    void shouldScanRoutesAddDependency(RuntimeType rt) throws Exception {
+        prepareMavenProject(rt);
+
+        // add arangodb to the route
+        addArangodbToCamelFile();
+
+        // run with --scan-routes
+        StringPrinter updatePrinter = new StringPrinter();
+        DependencyUpdate command = new DependencyUpdate(new 
CamelJBangMain().withPrinter(updatePrinter));
+        CommandLine.populateCommand(command,
+                "--scan-routes",
+                "--dir=" + workingDir,
+                new File(workingDir, "pom.xml").getAbsolutePath());
+        int exit = command.doCall();
+        Assertions.assertEquals(0, exit, updatePrinter.getLines().toString());
+
+        String pomContent = Files.readString(new File(workingDir, 
"pom.xml").toPath());
+        switch (rt) {
+            case quarkus:
+                assertThat(pomContent).contains("camel-quarkus-arangodb");
+                break;
+            case springBoot:
+                assertThat(pomContent).contains("camel-arangodb-starter");
+                break;
+            case main:
+                assertThat(pomContent).contains("camel-arangodb<");
+                break;
+        }
+    }
+
+    @Test
+    void shouldScanRoutesRemoveUnusedMavenDependency() throws Exception {
+        prepareMavenProject(RuntimeType.main);
+
+        // manually add camel-kafka to the pom.xml (which is not used by the 
route)
+        File pomFile = new File(workingDir, "pom.xml");
+        String pomContent = Files.readString(pomFile.toPath());
+        // insert before the closing </dependencies> tag in the main 
dependencies section
+        String kafkaDep = "        <dependency>\n"
+                          + "            <groupId>org.apache.camel</groupId>\n"
+                          + "            
<artifactId>camel-kafka</artifactId>\n"
+                          + "        </dependency>\n    ";
+        pomContent = pomContent.replaceFirst("(</dependencies>)", kafkaDep + 
"$1");
+        Files.writeString(pomFile.toPath(), pomContent);
+
+        // verify kafka is there
+        assertThat(Files.readString(pomFile.toPath())).contains("camel-kafka");
+
+        // run with --scan-routes to sync
+        StringPrinter updatePrinter = new StringPrinter();
+        DependencyUpdate command = new DependencyUpdate(new 
CamelJBangMain().withPrinter(updatePrinter));
+        CommandLine.populateCommand(command,
+                "--scan-routes",
+                "--dir=" + workingDir,
+                pomFile.getAbsolutePath());
+        int exit = command.doCall();
+        Assertions.assertEquals(0, exit, updatePrinter.getLines().toString());
+
+        // camel-kafka should be removed (not used in routes)
+        String updatedPom = Files.readString(pomFile.toPath());
+        assertThat(updatedPom).doesNotContain("camel-kafka");
+        // should still have camel-yaml-dsl (used for the route file)
+        assertThat(updatedPom).contains("camel-yaml-dsl");
+    }
+
+    @Test
+    void shouldPreserveNonCamelDepsInJBangFile() throws Exception {
+        // create a Java file with mixed Camel and non-Camel //DEPS
+        Path javaFile = createFile("MyRoute.java", """
+                ///usr/bin/env jbang
+                //DEPS org.apache.camel:camel-bom:4.13.0@pom
+                //DEPS org.apache.camel:camel-kafka
+                //DEPS com.google.guava:guava:33.0.0-jre
+                //DEPS io.netty:netty-all:4.1.100.Final
+                import org.apache.camel.builder.RouteBuilder;
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick").to("log:info");
+                    }
+                }
+                """);
+
+        // run scan-routes with just the Java file (no separate route files)
+        StringPrinter p = new StringPrinter();
+        DependencyUpdate command = new DependencyUpdate(new 
CamelJBangMain().withPrinter(p));
+        CommandLine.populateCommand(command,
+                "--scan-routes",
+                "--dir=" + workingDir,
+                javaFile.toAbsolutePath().toString());
+        int exit = command.doCall();
+        Assertions.assertEquals(0, exit, p.getLines().toString());
+
+        String content = Files.readString(javaFile);
+        // non-Camel deps should be preserved
+        assertThat(content).contains("//DEPS 
com.google.guava:guava:33.0.0-jre");
+        assertThat(content).contains("//DEPS 
io.netty:netty-all:4.1.100.Final");
+        // should have camel-bom (resolved fresh)
+        assertThat(content).contains("//DEPS org.apache.camel:camel-bom:");
+        // kafka should be removed (not used in the route definition)
+        assertThat(content).doesNotContain("camel-kafka");
+    }
+
+    @Test
+    void shouldScanRoutesIdempotentJBang() throws Exception {
+        Path javaFile = createFile("MyRoute.java", """
+                ///usr/bin/env jbang
+                //DEPS com.google.guava:guava:33.0.0-jre
+                import org.apache.camel.builder.RouteBuilder;
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick").to("log:info");
+                    }
+                }
+                """);
+
+        // run scan-routes twice
+        for (int run = 0; run < 2; run++) {
+            StringPrinter p = new StringPrinter();
+            DependencyUpdate command = new DependencyUpdate(new 
CamelJBangMain().withPrinter(p));
+            CommandLine.populateCommand(command,
+                    "--scan-routes",
+                    "--dir=" + workingDir,
+                    javaFile.toAbsolutePath().toString());
+            int exit = command.doCall();
+            Assertions.assertEquals(0, exit, p.getLines().toString());
+        }
+
+        String content = Files.readString(javaFile);
+        // should have exactly one camel-bom line
+        long bomCount = content.lines().filter(l -> 
l.contains("camel-bom")).count();
+        assertThat(bomCount).isEqualTo(1);
+        // non-Camel deps should still be there
+        assertThat(content).contains("//DEPS 
com.google.guava:guava:33.0.0-jre");
+    }
+
+    @Test
+    void shouldUpdateMultipleJBangFiles() throws Exception {
+        Path java1 = createFile("MyRoute1.java", """
+                ///usr/bin/env jbang
+                import org.apache.camel.builder.RouteBuilder;
+                public class MyRoute1 extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick").to("log:info");
+                    }
+                }
+                """);
+        Path java2 = createFile("MyRoute2.java", """
+                ///usr/bin/env jbang
+                import org.apache.camel.builder.RouteBuilder;
+                public class MyRoute2 extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick").to("log:info");
+                    }
+                }
+                """);
+
+        DependencyUpdate command = new DependencyUpdate(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command,
+                "--dir=" + workingDir,
+                java1.toAbsolutePath().toString(),
+                java2.toAbsolutePath().toString());
+
+        int exit = command.doCall();
+        Assertions.assertEquals(0, exit, printer.getLines().toString());
+
+        // both files should have //DEPS
+        String content1 = Files.readString(java1);
+        String content2 = Files.readString(java2);
+        assertThat(content1).contains("//DEPS org.apache.camel:camel-bom:");
+        assertThat(content2).contains("//DEPS org.apache.camel:camel-bom:");
+    }
+
+    private void prepareMavenProject(RuntimeType rt) throws Exception {
+        StringPrinter initPrinter = new StringPrinter();
+        Init initCommand = new Init(new 
CamelJBangMain().withPrinter(initPrinter));
+        String camelFilePath = new File(workingDir, 
"my.camel.yaml").getAbsolutePath();
+        CommandLine.populateCommand(initCommand, camelFilePath);
+        Assertions.assertEquals(0, initCommand.doCall(), 
initPrinter.getLines().toString());
+
+        StringPrinter exportPrinter = new StringPrinter();
+        Export exportCommand = new Export(new 
CamelJBangMain().withPrinter(exportPrinter));
+        CommandLine.populateCommand(exportCommand,
+                "--gav=examples:route:1.0.0",
+                "--dir=" + workingDir,
+                "--camel-version=4.13.0",
+                "--runtime=" + rt.runtime(),
+                camelFilePath);
+        Assertions.assertEquals(0, exportCommand.doCall(), 
exportPrinter.getLines().toString());
+    }
+
+    private void addArangodbToCamelFile() throws Exception {
+        File camelFile = new File(workingDir, 
"src/main/resources/camel/my.camel.yaml");
+        String content = Files.readString(camelFile.toPath());
+        content = content.replace("- log: ${body}", """
+                - to:
+                             uri: arangodb
+                             parameters:
+                               database: demo
+                """);
+        Files.writeString(camelFile.toPath(), content);
+    }
+
+    private Path createFile(String name, String content) throws Exception {
+        Path file = workingDir.toPath().resolve(name);
+        Files.writeString(file, content);
+        return file;
+    }
+
+    private static Stream<Arguments> runtimeProvider() {
+        Stream.Builder<Arguments> builder = Stream.builder();
+        builder.add(Arguments.of(RuntimeType.quarkus));
+        if (Runtime.version().feature() >= 21) {
+            builder.add(Arguments.of(RuntimeType.springBoot));
+        }
+        builder.add(Arguments.of(RuntimeType.main));
+        return builder.build();
+    }
+
+}


Reply via email to