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 18:14:36 UTC

(camel) branch fix/CAMEL-25833 created (now 923df16ebe54)

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

davsclaus pushed a change to branch fix/CAMEL-25833
in repository https://gitbox.apache.org/repos/asf/camel.git


      at 923df16ebe54 CAMEL-25833: camel-jbang - camel run --dev downloads a dependency added to camel.jbang.dependencies while the app runs

This branch includes the following new commits:

     new 923df16ebe54 CAMEL-25833: camel-jbang - camel run --dev downloads a dependency added to camel.jbang.dependencies while the app runs

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(camel) 01/01: CAMEL-25833: camel-jbang - camel run --dev downloads a dependency added to camel.jbang.dependencies while the app runs

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 923df16ebe54d778fdef8462fbb19cbd3da68e3d
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Mon Oct 19 20:14:14 2026 +0200

    CAMEL-25833: camel-jbang - camel run --dev downloads a dependency added to camel.jbang.dependencies while the app runs
    
    The dependencies in camel.jbang.dependencies were only downloaded when the application started. KameletMain now
    downloads the dependencies of a properties file that changed (camel.jbang.dependencies and
    camel.jbang.dependencies.main) before the beans and components of the reload are created, as the dependency of a
    //DEPS line in a source file is downloaded in a reload. A library that is on the classpath in another version is not
    downloaded, and a WARN says that a restart is needed to change the version.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../modules/ROOT/pages/camel-jbang-running.adoc    |   5 +
 .../java/org/apache/camel/main/KameletMain.java    |  13 +++
 .../main/download/ReloadDependencyDownloader.java  | 117 ++++++++++++++++++++
 .../download/ReloadDependencyDownloaderTest.java   | 123 +++++++++++++++++++++
 4 files changed, 258 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..b957067151b3 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,17 @@ 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)
+        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..43b1d763cc82
--- /dev/null
+++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/ReloadDependencyDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+     */
+    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;
+                        }
+                    }
+                });
+    }
+}