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 <[email protected]> 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) <[email protected]> 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; + } + } + }); + } +}
