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

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

commit 8b7855fe4316122afc325e3a455b858398614f03
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 23:03:56 2026 +0100

    CAMEL-26901: camel-jbang - camel run resolves the dependencies it is given 
together, with the breadth-first collector
    
    - The dependencies given with --dep or camel.jbang.dependencies are 
resolved together, as one dependency graph
      (DependencyDownloader.downloadDependencies). When they cannot be resolved 
together, they are resolved one at a
      time, so only the one that cannot be downloaded fails (or is logged when 
exporting).
    - camel-tooling-maven uses the breadth-first dependency collector of Maven 
Resolver with 10 threads, which downloads
      the POMs in parallel; it resolves the same dependencies as the 
depth-first default. The
      aether.dependencyCollector.impl and aether.dependencyCollector.bf.threads 
system properties still win.
    
    Cold camel run with camel:groovy, jsonpath, kafka, http and aws2-s3: 
8.4-9.2s instead of 12.8-13.8s, with the same
    65 JARs on the classpath. camel export writes the same project.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   8 ++
 .../modules/ROOT/pages/camel-jbang-running.adoc    |   5 +
 .../download/CommandLineDependencyDownloader.java  |   9 +-
 .../camel/main/download/DependencyDownloader.java  |  21 +++
 .../camel/main/download/DownloadThreadPool.java    |  54 +++++---
 .../main/download/MavenDependencyDownloader.java   |  71 ++++++++--
 .../CommandLineDependencyDownloaderTest.java       |   4 +
 .../download/DependencyDownloaderStrategyTest.java |   4 +
 .../MavenDependencyDownloaderBatchTest.java        | 145 +++++++++++++++++++++
 .../camel/tooling/maven/MavenDownloaderImpl.java   |  24 +++-
 .../tooling/maven/MavenDownloaderImplTest.java     |  14 ++
 11 files changed, 316 insertions(+), 43 deletions(-)

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
index 5b5aadf1caf3..5d7e07d47c96 100644
--- 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
@@ -2528,6 +2528,14 @@ stops sending no longer hangs the download for that 
long. Set the `aether.connec
 (in millis) for a longer timeout. A repository that cannot be connected to is 
now logged with a WARN and not used
 for the rest of the run.
 
+=== camel-jbang - dependencies are resolved with the breadth-first collector 
of Maven Resolver
+
+The Camel CLI (and other tools that download with `camel-tooling-maven`) now 
resolves dependencies with the
+breadth-first collector of Maven Resolver 
(`aether.dependencyCollector.impl=bf`), which downloads the POMs in parallel,
+instead of the depth-first one Maven uses by default. It resolves the same 
dependencies; a cold `camel run` with five
+components downloads them about a third faster. Set the 
`aether.dependencyCollector.impl=df` system property to go back.
+The dependencies given with `--dep` or `camel.jbang.dependencies` are now 
resolved together, as one dependency graph.
+
 === camel-pqc - KeyLifecycleManager behaviour aligned across the 
implementations
 
 Four differences between the `KeyLifecycleManager` implementations are 
resolved. All four are corrections, but each
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
index 6b6ea089b531..8944717e5ab4 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
@@ -348,6 +348,11 @@ To disable automatic downloading:
 camel run foo.java --download=false
 ----
 
+The dependencies given with `--dep` or `camel.jbang.dependencies` are resolved 
together, as one dependency graph, and
+Camel CLI resolves with the breadth-first collector of Maven Resolver, which 
downloads the POMs in parallel (10
+threads). Set the `aether.dependencyCollector.impl=df` system property to use 
the depth-first collector of Maven
+instead.
+
 A repository that cannot be connected to (the host does not resolve, refuses 
the connection, or does not answer
 within the connect timeout of 10 seconds) is logged once with a WARN and not 
used for the rest of the run, so each
 download does not wait for it again. A repository that stops sending data 
fails the download after 60 seconds.
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/CommandLineDependencyDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/CommandLineDependencyDownloader.java
index 04a8d21adff7..736c9fdb0d70 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/CommandLineDependencyDownloader.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/CommandLineDependencyDownloader.java
@@ -61,13 +61,8 @@ public class CommandLineDependencyDownloader extends 
ServiceSupport {
         }
 
         if (!gavs.isEmpty()) {
-            for (MavenGav gav : gavs) {
-                if (required) {
-                    downloader.downloadRequiredDependency(gav.getGroupId(), 
gav.getArtifactId(), gav.getVersion());
-                } else {
-                    downloader.downloadDependency(gav.getGroupId(), 
gav.getArtifactId(), gav.getVersion());
-                }
-            }
+            // together, so they are downloaded in parallel
+            downloader.downloadDependencies(gavs, required);
         }
     }
 
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
index ad118a5a2deb..3217abeca445 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
@@ -23,6 +23,7 @@ import org.apache.camel.CamelContextAware;
 import org.apache.camel.StaticService;
 import org.apache.camel.tooling.maven.MavenArtifact;
 import org.apache.camel.tooling.maven.MavenArtifactDescriptor;
+import org.apache.camel.tooling.maven.MavenGav;
 import org.apache.camel.tooling.maven.RepositoryResolver;
 
 /**
@@ -170,6 +171,26 @@ public interface DependencyDownloader extends 
CamelContextAware, StaticService {
      */
     void downloadRequiredDependency(String groupId, String artifactId, String 
version);
 
+    /**
+     * Downloads the dependencies incl transitive dependencies, resolved 
together as one dependency graph, so their JARs
+     * are downloaded in parallel. When they cannot be downloaded together, 
they are downloaded one at a time.
+     *
+     * @param  gavs              the dependencies
+     * @param  required          whether a dependency that cannot be 
downloaded fails, as
+     *                           {@link #downloadRequiredDependency(String, 
String, String)}, instead of logging the
+     *                           error
+     * @throws DownloadException when required and a dependency cannot be 
downloaded
+     */
+    default void downloadDependencies(List<MavenGav> gavs, boolean required) {
+        for (MavenGav gav : gavs) {
+            if (required) {
+                downloadRequiredDependency(gav.getGroupId(), 
gav.getArtifactId(), gav.getVersion());
+            } else {
+                downloadDependency(gav.getGroupId(), gav.getArtifactId(), 
gav.getVersion());
+            }
+        }
+    }
+
     /**
      * Downloads as hidden dependency that are not captured as a requirement
      *
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DownloadThreadPool.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DownloadThreadPool.java
index 1e399a62e496..faecbd73d034 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DownloadThreadPool.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DownloadThreadPool.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.main.download;
 
+import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -75,15 +76,30 @@ class DownloadThreadPool extends ServiceSupport implements 
CamelContextAware {
      *                 error
      */
     public void download(Logger log, Runnable task, String gav, boolean 
required) {
+        download(log, task, List.of(gav), required);
+    }
+
+    /**
+     * Downloads dependencies together, and waits for the download to complete.
+     *
+     * @param required whether to throw a {@link DownloadException} when the 
download fails, instead of logging the
+     *                 error
+     */
+    public void download(Logger log, Runnable task, List<String> gavs, boolean 
required) {
         Future<?> future = executorService.submit(task);
-        awaitCompletion(log, future, gav, required);
+        awaitCompletion(log, future, gavs, required);
     }
 
     void awaitCompletion(Logger log, Future<?> future, String gav) {
-        awaitCompletion(log, future, gav, false);
+        awaitCompletion(log, future, List.of(gav), false);
     }
 
     void awaitCompletion(Logger log, Future<?> future, String gav, boolean 
required) {
+        awaitCompletion(log, future, List.of(gav), required);
+    }
+
+    void awaitCompletion(Logger log, Future<?> future, List<String> gavs, 
boolean required) {
+        String gav = String.join(", ", gavs);
         StopWatch watch = new StopWatch();
         boolean done = false;
         while (!done) {
@@ -113,24 +129,26 @@ class DownloadThreadPool extends ServiceSupport 
implements CamelContextAware {
             }
         }
 
-        MavenGav a = MavenGav.parseGav(gav);
-        DownloadRecord downloadRecord = 
downloader.getDownloadState(a.getGroupId(), a.getArtifactId(), a.getVersion());
-        if (downloadRecord != null) {
-            long taken = watch.taken();
-            String url = downloadRecord.repoUrl();
-            String id = downloadRecord.repoId();
-            String msg = "Downloaded: " + gav + " (took: "
-                         + TimeUtils.printDuration(taken, true) + ") from: " + 
id + "@" + url;
-            log.info(msg);
-        } else {
-            long taken = watch.taken();
-            String msg = "Resolved: " + gav + " (took: "
-                         + TimeUtils.printDuration(taken, true) + ")";
-            if (verbose || taken > 2000) {
-                // slow resolving then log
+        long taken = watch.taken();
+        for (String g : gavs) {
+            MavenGav a = MavenGav.parseGav(g);
+            DownloadRecord downloadRecord
+                    = downloader.getDownloadState(a.getGroupId(), 
a.getArtifactId(), a.getVersion());
+            if (downloadRecord != null) {
+                String url = downloadRecord.repoUrl();
+                String id = downloadRecord.repoId();
+                String msg = "Downloaded: " + g + " (took: "
+                             + TimeUtils.printDuration(taken, true) + ") from: 
" + id + "@" + url;
                 log.info(msg);
             } else {
-                log.debug(msg);
+                String msg = "Resolved: " + g + " (took: "
+                             + TimeUtils.printDuration(taken, true) + ")";
+                if (verbose || taken > 2000) {
+                    // slow resolving then log
+                    log.info(msg);
+                } else {
+                    log.debug(msg);
+                }
             }
         }
     }
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
index 215cd1f0fe8c..8749c51a6483 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
@@ -303,7 +303,44 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
             String parentGav,
             String groupId, String artifactId, String version, boolean 
transitively,
             boolean hidden, String extraRepos, boolean required) {
+        MavenGav gav = prepare(groupId, artifactId, version, hidden);
+        if (gav != null) {
+            doDownload(parentGav, List.of(gav), transitively, extraRepos, 
required);
+        }
+    }
+
+    @Override
+    public void downloadDependencies(List<MavenGav> gavs, boolean required) {
+        List<MavenGav> todo = new ArrayList<>();
+        for (MavenGav gav : gavs) {
+            MavenGav prepared = prepare(gav.getGroupId(), gav.getArtifactId(), 
gav.getVersion(), false);
+            if (prepared != null) {
+                todo.add(prepared);
+            }
+        }
+        if (todo.size() > 1) {
+            try {
+                // together: one dependency graph, and the JARs are downloaded 
in parallel
+                doDownload(null, todo, true, null, true);
+                return;
+            } catch (DownloadException e) {
+                // one at a time, so only the one that cannot be downloaded 
fails (or is logged)
+                LOG.debug("Cannot download the dependencies together, 
downloading them one at a time: {}",
+                        e.getMessage());
+            }
+        }
+        for (MavenGav gav : todo) {
+            doDownload(null, List.of(gav), true, null, required);
+        }
+    }
 
+    /**
+     * Prepares a dependency to download: resolves its version and triggers 
the listeners.
+     *
+     * @return the dependency to download, or null when there is nothing to 
download (it is on the classpath, or has no
+     *         version)
+     */
+    private MavenGav prepare(String groupId, String artifactId, String 
version, boolean hidden) {
         if (versionResolver != null && version != null) {
             version = versionResolver.resolve(version);
         }
@@ -326,22 +363,24 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
             }
             if (bootClasspath.contains(target)) {
                 // already on classpath
-                return;
+                return null;
             }
         }
 
         // we need version to be able to download from maven
         if (ObjectHelper.isEmpty(version)) {
-            return;
+            return null;
         }
+        return MavenGav.fromCoordinates(groupId, artifactId, version, null, 
null);
+    }
 
-        String gav = groupId + ":" + artifactId + ":" + version;
-        String targetVersion = version;
+    private void doDownload(
+            String parentGav, List<MavenGav> gavs, boolean transitively, 
String extraRepos, boolean required) {
+        List<String> deps = gavs.stream().map(MavenGav::toString).toList();
         threadPool.download(LOG, () -> {
-            List<String> deps = List.of(gav);
-
             // include Apache snapshot to make it easy to use upcoming releases
-            boolean useApacheSnapshots = "org.apache.camel".equals(groupId) && 
targetVersion.contains("SNAPSHOT");
+            boolean useApacheSnapshots = gavs.stream()
+                    .anyMatch(g -> "org.apache.camel".equals(g.getGroupId()) 
&& g.getVersion().contains("SNAPSHOT"));
 
             // include extra repositories (if any) - these will be used in 
addition
             // to the ones detected from ~/.m2/settings.xml and configured in
@@ -349,8 +388,10 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
             Set<String> extraRepositories = new 
LinkedHashSet<>(resolveExtraRepositories(extraRepos));
             if (knownReposResolver != null) {
                 // and from known extra repositories (if any)
-                String known = knownReposResolver.getRepo(artifactId);
-                extraRepositories.addAll(resolveExtraRepositories(known));
+                for (MavenGav g : gavs) {
+                    String known = 
knownReposResolver.getRepo(g.getArtifactId());
+                    extraRepositories.addAll(resolveExtraRepositories(known));
+                }
             }
 
             // what is already on the classpath is left out (with its 
dependencies), so it is not downloaded
@@ -365,9 +406,9 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
             }
             List<File> files = new ArrayList<>();
             if (verbose) {
-                LOG.info("Dependencies: {} -> [{}]", gav, artifacts);
+                LOG.info("Dependencies: {} -> [{}]", String.join(", ", deps), 
artifacts);
             } else {
-                LOG.debug("Dependencies: {} -> [{}]", gav, artifacts);
+                LOG.debug("Dependencies: {} -> [{}]", String.join(", ", deps), 
artifacts);
             }
 
             for (MavenArtifact a : artifacts) {
@@ -395,8 +436,10 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
                 }
             }
             if (!artifacts.isEmpty()) {
-                for (DownloadListener listener : downloadListeners) {
-                    listener.onDownloadedDependency(groupId, artifactId, 
targetVersion);
+                for (MavenGav g : gavs) {
+                    for (DownloadListener listener : downloadListeners) {
+                        listener.onDownloadedDependency(g.getGroupId(), 
g.getArtifactId(), g.getVersion());
+                    }
                 }
             }
             if (!extraRepositories.isEmpty()) {
@@ -407,7 +450,7 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
                 }
             }
 
-        }, gav, required);
+        }, deps, required);
     }
 
     @Override
diff --git 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/CommandLineDependencyDownloaderTest.java
 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/CommandLineDependencyDownloaderTest.java
index 556dce5ef25c..3fd667d94d7c 100644
--- 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/CommandLineDependencyDownloaderTest.java
+++ 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/CommandLineDependencyDownloaderTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.main.download;
 
+import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Proxy;
 import java.util.ArrayList;
 import java.util.List;
@@ -111,6 +112,9 @@ class CommandLineDependencyDownloaderTest {
                             downloaded.add(args[0] + ":" + args[1] + ":" + 
args[2]);
                             return null;
                         }
+                        case "downloadDependencies" -> {
+                            return InvocationHandler.invokeDefault(proxy, 
method, args);
+                        }
                         case "downloadRequiredDependency" -> {
                             downloaded.add("required:" + args[0] + ":" + 
args[1] + ":" + args[2]);
                             if (requiredFailure != null) {
diff --git 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderStrategyTest.java
 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderStrategyTest.java
index 5a8a222df17e..da6fe714ce99 100644
--- 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderStrategyTest.java
+++ 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderStrategyTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.main.download;
 
+import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Proxy;
 import java.util.ArrayList;
 import java.util.List;
@@ -76,6 +77,9 @@ class DependencyDownloaderStrategyTest {
                             downloaded.add(args[0] + ":" + args[1] + ":" + 
args[2]);
                             return null;
                         }
+                        case "downloadDependencies" -> {
+                            return InvocationHandler.invokeDefault(proxy, 
method, args);
+                        }
                         case "downloadRequiredDependency" -> {
                             downloaded.add("required:" + args[0] + ":" + 
args[1] + ":" + args[2]);
                             if (requiredFailure != null) {
diff --git 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/MavenDependencyDownloaderBatchTest.java
 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/MavenDependencyDownloaderBatchTest.java
new file mode 100644
index 000000000000..e48a85f39b17
--- /dev/null
+++ 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/MavenDependencyDownloaderBatchTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.main.download;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.tooling.maven.MavenGav;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Dependencies downloaded together, from a local file repository.
+ */
+class MavenDependencyDownloaderBatchTest {
+
+    @TempDir
+    Path dir;
+
+    private MavenDependencyDownloader downloader;
+    private DependencyDownloaderClassLoader classLoader;
+    private final List<String> downloaded = new ArrayList<>();
+
+    @BeforeEach
+    void setUp() throws Exception {
+        Path repo = Files.createDirectories(dir.resolve("repo"));
+        // a -> common 1.0, b -> common 2.0
+        writeJar(repo, "a", "1.0", "common", "1.0");
+        writeJar(repo, "b", "1.0", "common", "2.0");
+        writeJar(repo, "common", "1.0", null, null);
+        writeJar(repo, "common", "2.0", null, null);
+        Path settings = Files.writeString(dir.resolve("settings.xml"),
+                "<settings><localRepository>" + dir.resolve("local") + 
"</localRepository></settings>");
+
+        classLoader = new 
DependencyDownloaderClassLoader(getClass().getClassLoader());
+        downloader = new MavenDependencyDownloader();
+        downloader.setClassLoader(classLoader);
+        downloader.setMavenCentralEnabled(false);
+        downloader.setMavenApacheSnapshotEnabled(false);
+        downloader.setMavenSettings(settings.toString());
+        downloader.setRepositories(repo.toUri().toString());
+        downloader.addDownloadListener(new DownloadListener() {
+            @Override
+            public void onDownloadDependency(String groupId, String 
artifactId, String version) {
+            }
+
+            @Override
+            public void onDownloadedDependency(String groupId, String 
artifactId, String version) {
+                downloaded.add(groupId + ":" + artifactId + ":" + version);
+            }
+
+            @Override
+            public void onAlreadyDownloadedDependency(String groupId, String 
artifactId, String version) {
+            }
+        });
+        downloader.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        downloader.stop();
+        classLoader.close();
+    }
+
+    @Test
+    void dependenciesAreResolvedTogether() {
+        downloader.downloadDependencies(List.of(gav("a"), gav("b")), true);
+
+        assertEquals(List.of("org.example.batch:a:1.0", 
"org.example.batch:b:1.0"), downloaded);
+        // one graph: one version of common (the nearest first: a's)
+        assertEquals(List.of("a-1.0.jar", "b-1.0.jar", "common-1.0.jar"), 
jars());
+    }
+
+    @Test
+    void dependencyThatCannotBeDownloadedIsLogged() {
+        downloader.downloadDependencies(List.of(gav("a"), gav("missing"), 
gav("b")), false);
+
+        // one at a time: the others are downloaded
+        assertEquals(List.of("org.example.batch:a:1.0", 
"org.example.batch:b:1.0"), downloaded);
+        assertTrue(jars().containsAll(List.of("a-1.0.jar", "b-1.0.jar")), 
jars().toString());
+    }
+
+    @Test
+    void requiredDependencyThatCannotBeDownloadedFails() {
+        DownloadException e = assertThrows(DownloadException.class,
+                () -> downloader.downloadDependencies(List.of(gav("a"), 
gav("missing"), gav("b")), true));
+
+        // the one that cannot be downloaded
+        assertTrue(e.getMessage().startsWith("Cannot download dependency: 
org.example.batch:missing:1.0 due to: "),
+                e.getMessage());
+        assertEquals(List.of("org.example.batch:a:1.0"), downloaded);
+    }
+
+    private static MavenGav gav(String artifactId) {
+        return MavenGav.fromCoordinates("org.example.batch", artifactId, 
"1.0", null, null);
+    }
+
+    private List<String> jars() {
+        return classLoader.getDownloaded().stream().sorted().toList();
+    }
+
+    private static void writeJar(Path repo, String artifactId, String version, 
String dependency, String dependencyVersion)
+            throws Exception {
+        Path dir = Files.createDirectories(repo.resolve("org/example/batch/" + 
artifactId + "/" + version));
+        String deps = dependency == null
+                ? ""
+                : """
+                          <dependencies>
+                            
<dependency><groupId>org.example.batch</groupId><artifactId>%s</artifactId><version>%s</version></dependency>
+                          </dependencies>
+                        """
+                        .formatted(dependency, dependencyVersion);
+        Files.writeString(dir.resolve(artifactId + "-" + version + ".pom"), """
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                  <modelVersion>4.0.0</modelVersion>
+                  <groupId>org.example.batch</groupId>
+                  <artifactId>%s</artifactId>
+                  <version>%s</version>
+                %s</project>
+                """.formatted(artifactId, version, deps));
+        Files.writeString(dir.resolve(artifactId + "-" + version + ".jar"), 
artifactId);
+    }
+}
diff --git 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
index d720e4544a2f..9a0e0b08f81f 100644
--- 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
+++ 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
@@ -124,6 +124,10 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
     private static final String UPDATE_CHECK_SESSION_STATE = 
"aether.updateCheckManager.sessionState";
     private static final DependencyFilter ACCEPT_ALL = (node, parents) -> true;
     private static final int DEFAULT_REQUEST_TIMEOUT = 60000;
+    // see org.eclipse.aether.internal.impl.collect.DefaultDependencyCollector 
and BfDependencyCollector
+    private static final String COLLECTOR_IMPL = 
"aether.dependencyCollector.impl";
+    private static final String COLLECTOR_BF_THREADS = 
"aether.dependencyCollector.bf.threads";
+    private static final int COLLECTOR_THREADS = 10;
 
     // Configuration
     private boolean mavenCentralEnabled = true;
@@ -373,7 +377,7 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
 
         // Get RepositorySystem and RepositorySystemSession from MIMA
         repositorySystem = mimaContext.repositorySystem();
-        repositorySystemSession = 
withTimeoutsAndReachability(withTrackingKeyCompat(mimaContext.repositorySystemSession()));
+        repositorySystemSession = 
withCamelDefaults(withTrackingKeyCompat(mimaContext.repositorySystemSession()));
 
         // Configure repositories (same as embedded mode)
         List<RemoteRepository> originalRepositories = new ArrayList<>();
@@ -865,14 +869,22 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
     }
 
     /**
-     * Sets the request timeout of Camel (unless configured, such as with the 
aether.connector.requestTimeout system
-     * property), and stops using a repository that cannot be connected to.
+     * Sets the defaults of Camel (unless configured, such as with the 
aether.connector.requestTimeout system property):
+     * the request timeout, and the breadth-first dependency collector, which 
downloads the POMs in parallel (it
+     * resolves the same dependencies as the depth-first default of Maven 
Resolver). And stops using a repository that
+     * cannot be connected to.
      */
-    private RepositorySystemSession 
withTimeoutsAndReachability(RepositorySystemSession session) {
+    private RepositorySystemSession withCamelDefaults(RepositorySystemSession 
session) {
         DefaultRepositorySystemSession answer = new 
DefaultRepositorySystemSession(session);
         if 
(session.getConfigProperties().get(ConfigurationProperties.REQUEST_TIMEOUT) == 
null) {
             answer.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 
requestTimeout);
         }
+        if (session.getConfigProperties().get(COLLECTOR_IMPL) == null) {
+            answer.setConfigProperty(COLLECTOR_IMPL, "bf");
+        }
+        if (session.getConfigProperties().get(COLLECTOR_BF_THREADS) == null) {
+            answer.setConfigProperty(COLLECTOR_BF_THREADS, COLLECTOR_THREADS);
+        }
         
answer.setTransferListener(ChainedTransferListener.newInstance(session.getTransferListener(),
                 new UnreachableRepositoryListener(unreachableRepositories)));
         return answer;
@@ -942,6 +954,10 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
         return unreachableRepositories;
     }
 
+    RepositorySystemSession getRepositorySystemSession() {
+        return repositorySystemSession;
+    }
+
     @Override
     public MavenArtifactDescriptor resolveArtifactDescriptor(String gav, 
Set<String> extraRepositories)
             throws MavenResolutionException {
diff --git 
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
 
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
index 61a8e5568299..21c9a7bfe53f 100644
--- 
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
+++ 
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
@@ -28,6 +28,7 @@ import java.nio.file.Files;
 import java.security.MessageDigest;
 import java.util.Base64;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -1244,4 +1245,17 @@ class MavenDownloaderImplTest {
         
assertNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(new 
IOException("Not found")));
         
assertNull(MavenDownloaderImpl.UnreachableRepositoryListener.connectFailure(null));
     }
+
+    @Test
+    void testCamelDefaults() throws Exception {
+        try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+            downloader.setMavenSettingsLocation("false");
+            downloader.build();
+
+            Map<String, Object> config = 
downloader.getRepositorySystemSession().getConfigProperties();
+            assertEquals(60000, config.get("aether.connector.requestTimeout"));
+            assertEquals("bf", config.get("aether.dependencyCollector.impl"));
+            assertEquals(10, 
config.get("aether.dependencyCollector.bf.threads"));
+        }
+    }
 }

Reply via email to