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;
+ }
+ }
+ });
+ }
+}