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 93ffd6387711 CAMEL-25833: camel-jbang - camel run --dev downloads a 
dependency added to camel.jbang.dependencies while the app runs (#28126)
93ffd6387711 is described below

commit 93ffd63877113f074a38cad9cf7ee2d842448a53
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 19 21:51:16 2026 +0200

    CAMEL-25833: camel-jbang - camel run --dev downloads a dependency added to 
camel.jbang.dependencies while the app runs (#28126)
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-running.adoc    |   5 +
 .../java/org/apache/camel/main/KameletMain.java    |  16 +++
 .../main/download/ReloadDependencyDownloader.java  | 119 ++++++++++++++++++++
 .../download/ReloadDependencyDownloaderTest.java   | 123 +++++++++++++++++++++
 4 files changed, 263 insertions(+)

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 bcf3613c7fef..167ddd692799 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
@@ -229,6 +229,11 @@ or of `--source-dir`, and from the properties files given 
on the command line or
 for example `camel run camel/*.yaml camel/application.properties` run from the 
parent directory of `camel`.
 They are added to the dependencies given with `--dep`.
 
+In dev mode (`--dev`), a dependency that is added to 
`camel.jbang.dependencies` (or `camel.jbang.dependencies.main`)
+while the application runs is downloaded when the properties file is reloaded, 
before the beans and components
+of the file are created. To change the version of a library that is on the 
classpath, or to remove a library,
+restart the application.
+
 == Including resource directories
 
 To include entire directories of resource files (such as XSD schemas, WSDL 
files, etc.)
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
index 8be7dadd91e6..e65becbd5562 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
@@ -75,6 +75,7 @@ import org.apache.camel.main.download.KnownReposResolver;
 import org.apache.camel.main.download.MavenDependencyDownloader;
 import org.apache.camel.main.download.PackageNameSourceLoader;
 import org.apache.camel.main.download.PromptPropertyPlaceholderSource;
+import org.apache.camel.main.download.ReloadDependencyDownloader;
 import org.apache.camel.main.download.SagaDownloader;
 import org.apache.camel.main.download.StubComponentAutowireStrategy;
 import org.apache.camel.main.download.TransactedDownloader;
@@ -118,6 +119,7 @@ import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.startup.BacklogStartupStepRecorder;
 import org.apache.camel.tooling.maven.MavenGav;
 import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.OrderedLocationProperties;
 
 /**
  * A Main class for booting up Camel with Kamelet in standalone mode.
@@ -883,6 +885,20 @@ public class KameletMain extends MainCommandLineSupport {
         super.autoconfigure(camelContext);
     }
 
+    @Override
+    protected void autoConfigurationFromReloadedProperties(
+            CamelContext camelContext, OrderedLocationProperties 
reloadedProperties, boolean changed)
+            throws Exception {
+        // a dependency added to a properties file while the application runs 
is downloaded before the beans and
+        // components of the reload are created (CAMEL-25833). Not when 
options are applied again, as a context reload
+        // does for secret rotation: that does not add dependencies
+        if (changed) {
+            new 
ReloadDependencyDownloader(camelContext).download(reloadedProperties,
+                    getInstanceType() + ".dependencies", getInstanceType() + 
".dependencies.main");
+        }
+        super.autoConfigurationFromReloadedProperties(camelContext, 
reloadedProperties, changed);
+    }
+
     @Override
     protected LifecycleStrategy createLifecycleStrategy(CamelContext 
camelContext) {
         return new KameletAutowiredLifecycleStrategy(camelContext, 
stubPattern, silent);
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/ReloadDependencyDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/ReloadDependencyDownloader.java
new file mode 100644
index 000000000000..d4a580d83340
--- /dev/null
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/ReloadDependencyDownloader.java
@@ -0,0 +1,119 @@
+/*
+ * 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.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.tooling.maven.MavenGav;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Downloads the dependencies of a properties file that changed while the 
application runs, such as a
+ * camel.jbang.dependencies line added to application.properties in dev mode, 
so the beans and components of the reload
+ * can use them. The dependencies of the properties files are otherwise only 
downloaded when the application starts
+ * ({@link CommandLineDependencyDownloader}), while a //DEPS line in a source 
file is downloaded in a reload as well.
+ * <p/>
+ * A library that is on the classpath in another version is not downloaded, as 
both versions would then be on the
+ * classpath: changing the version needs a restart.
+ */
+public final class ReloadDependencyDownloader {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ReloadDependencyDownloader.class);
+
+    private final CamelContext camelContext;
+    private final DependencyDownloader downloader;
+
+    public ReloadDependencyDownloader(CamelContext camelContext) {
+        this.camelContext = camelContext;
+        this.downloader = camelContext.hasService(DependencyDownloader.class);
+    }
+
+    /**
+     * Downloads the dependencies in the given keys of the properties (comma 
separated, as when the application starts)
+     * that are not on the classpath.
+     *
+     * @return the dependencies that are not downloaded as another version of 
them is on the classpath, which need a
+     *         restart
+     */
+    public List<String> download(Properties properties, String... keys) {
+        List<String> restart = new ArrayList<>();
+        if (downloader == null) {
+            return restart;
+        }
+        for (String key : keys) {
+            String value = properties.getProperty(key);
+            if (value == null) {
+                continue;
+            }
+            for (String dep : value.split(",")) {
+                dep = dep.trim();
+                if (dep.isEmpty()) {
+                    continue;
+                }
+                MavenGav gav = CatalogDependencyResolver.resolve(dep, 
camelContext.getVersion());
+                String groupId = gav.getGroupId();
+                String artifactId = gav.getArtifactId();
+                String version = gav.getVersion();
+                if (artifactId == null || 
downloader.alreadyOnClasspath(groupId, artifactId, version)) {
+                    continue;
+                }
+                String loaded = loadedVersion(groupId, artifactId);
+                if (loaded != null) {
+                    LOG.warn("The dependency {}:{}:{} in {} is not downloaded, 
as version {} is on the classpath:"
+                             + " restart to change the version",
+                            groupId, artifactId, version, key, loaded);
+                    restart.add(groupId + ":" + artifactId + ":" + version);
+                    continue;
+                }
+                LOG.info("Downloading the dependency {}:{}:{} added to {}", 
groupId, artifactId, version, key);
+                downloader.downloadDependency(groupId, artifactId, version);
+            }
+        }
+        return restart;
+    }
+
+    /**
+     * The version of the library that the downloader has put on the 
classpath, from the path of its JAR in the Maven
+     * repository (such as 
.../com/github/javafaker/javafaker/1.0.1/javafaker-1.0.1.jar), or null. Only a 
JAR in the
+     * layout of a Maven repository is recognized, which is where the 
downloader puts them; for a JAR elsewhere, the
+     * dependency is downloaded as when this check did not exist.
+     */
+    private String loadedVersion(String groupId, String artifactId) {
+        if (downloader.getClassLoader() instanceof URLClassLoader ucl) {
+            String dir = "/" + groupId.replace('.', '/') + "/" + artifactId + 
"/";
+            for (URL url : ucl.getURLs()) {
+                String path = url.toString();
+                int pos = path.indexOf(dir);
+                if (pos < 0) {
+                    continue;
+                }
+                String rest = path.substring(pos + dir.length());
+                int slash = rest.indexOf('/');
+                if (slash > 0 && rest.startsWith(artifactId + "-", slash + 1)) 
{
+                    return rest.substring(0, slash);
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/ReloadDependencyDownloaderTest.java
 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/ReloadDependencyDownloaderTest.java
new file mode 100644
index 000000000000..a51225373ead
--- /dev/null
+++ 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/ReloadDependencyDownloaderTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.lang.reflect.Proxy;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.camel.impl.engine.SimpleCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * CAMEL-25833: a dependency added to camel.jbang.dependencies while the 
application runs in dev mode is downloaded in
+ * the reload, as the dependencies are when the application starts.
+ */
+class ReloadDependencyDownloaderTest {
+
+    private static final String[] KEYS = { "camel.jbang.dependencies", 
"camel.jbang.dependencies.main" };
+
+    private final List<String> downloaded = new ArrayList<>();
+
+    @Test
+    void theDependenciesThatAreNotOnTheClasspathAreDownloaded() throws 
Exception {
+        Properties properties = new Properties();
+        properties.setProperty("camel.jbang.dependencies", 
"com.github.javafaker:javafaker:1.0.2, org.acme:loaded:1.0,");
+        properties.setProperty("camel.jbang.dependencies.main", 
"org.acme:main-only:2.0");
+        properties.setProperty("camel.beans.faker", 
"#class:com.github.javafaker.Faker");
+
+        List<String> restart = download(properties, 
Set.of("org.acme:loaded:1.0"));
+
+        assertEquals(List.of("com.github.javafaker:javafaker:1.0.2", 
"org.acme:main-only:2.0"), downloaded);
+        assertEquals(List.of(), restart);
+    }
+
+    @Test
+    void aLibraryOnTheClasspathInAnotherVersionIsNotDownloaded() throws 
Exception {
+        Properties properties = new Properties();
+        properties.setProperty("camel.jbang.dependencies", 
"com.github.javafaker:javafaker:1.0.2");
+
+        List<String> restart = download(properties, Set.of(),
+                
URI.create("file:/repo/com/github/javafaker/javafaker/1.0.1/javafaker-1.0.1.jar").toURL());
+
+        assertEquals(List.of(), downloaded);
+        assertEquals(List.of("com.github.javafaker:javafaker:1.0.2"), restart);
+    }
+
+    @Test
+    void propertiesWithoutDependenciesDownloadNothing() throws Exception {
+        Properties properties = new Properties();
+        properties.setProperty("camel.component.log.level", "WARN");
+
+        List<String> restart = download(properties, Set.of());
+
+        assertEquals(List.of(), downloaded);
+        assertEquals(List.of(), restart);
+    }
+
+    private List<String> download(Properties properties, Set<String> 
onClasspath, URL... loaded) throws Exception {
+        try (SimpleCamelContext context = new SimpleCamelContext();
+             URLClassLoader classLoader = new URLClassLoader(loaded, null)) {
+            context.addService(recordingDownloader(context, onClasspath, 
classLoader));
+            return new 
ReloadDependencyDownloader(context).download(properties, KEYS);
+        }
+    }
+
+    // records the downloads; the given GAVs are on the classpath, and the 
class loader has the JARs downloaded before
+    private DependencyDownloader recordingDownloader(
+            SimpleCamelContext context, Set<String> onClasspath, ClassLoader 
classLoader) {
+        return (DependencyDownloader) Proxy.newProxyInstance(
+                ReloadDependencyDownloaderTest.class.getClassLoader(),
+                new Class[] { DependencyDownloader.class },
+                (proxy, method, args) -> {
+                    switch (method.getName()) {
+                        case "hashCode" -> {
+                            return System.identityHashCode(proxy);
+                        }
+                        case "equals" -> {
+                            return proxy == args[0];
+                        }
+                        case "toString" -> {
+                            return "RecordingDependencyDownloader";
+                        }
+                        case "downloadDependency" -> {
+                            downloaded.add(args[0] + ":" + args[1] + ":" + 
args[2]);
+                            return null;
+                        }
+                        case "alreadyOnClasspath" -> {
+                            return onClasspath.contains(args[0] + ":" + 
args[1] + ":" + args[2]);
+                        }
+                        case "getClassLoader" -> {
+                            return classLoader;
+                        }
+                        case "getCamelContext" -> {
+                            return context;
+                        }
+                        default -> {
+                            return method.getReturnType() == boolean.class ? 
Boolean.FALSE : null;
+                        }
+                    }
+                });
+    }
+}

Reply via email to