You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/19 19:51:23 UTC

(camel) branch main updated: CAMEL-25833: camel-jbang - camel run --dev downloads a dependency added to camel.jbang.dependencies while the app runs (#28126)

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 <cl...@gmail.com>
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) <no...@anthropic.com>
---
 .../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;
+                        }
+                    }
+                });
+    }
+}