This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25239 in repository https://gitbox.apache.org/repos/asf/camel.git
commit c657f0e5ab207486e9214614ba56a4f4543e2bfc Author: Claus Ibsen <[email protected]> AuthorDate: Fri Oct 2 14:51:32 2026 +0200 CAMEL-25239: camel-kamelet-main - an import of any class of a Camel component downloads the component A Java or Groovy source that imports a class of a Camel component, other than the component class itself, failed to compile in camel run and camel export, for example a route that imports org.apache.camel.component.aws2.s3.AWS2S3Constants for the headers of the aws-s3-source Kamelet. The known dependencies map each component by its component class only, so the import did not resolve, and the source failed to compile before the Kamelet that would bring the component was loaded. camel export then failed, or with --ignore-loading-error wrote a pom without the Kamelet's dependencies. The known imports now also find a component by its package (packages shared by different dependencies and base packages such as org.apache.camel are skipped). Only imports use it: a class that is only looked up while running still needs the component class itself, so probing for a class does not download a component. Reported by smongiar in #27206. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../modules/ROOT/pages/camel-jbang-running.adoc | 7 +++ .../main/download/JavaKnownImportsDownloader.java | 2 +- .../main/download/KnownDependenciesResolver.java | 63 +++++++++++++++++++--- .../download/KnownDependenciesResolverTest.java | 34 ++++++++++++ 4 files changed, 98 insertions(+), 8 deletions(-) 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 9af930c01171..f4aa1f4799b8 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc @@ -277,6 +277,13 @@ The lookup walks up from the class name: 4. The dependency and its transitive dependencies are downloaded and added to the classpath, and the class is loaded again. +For an `import` in a Java or Groovy source, a Camel component is also found by its package, so +importing any class of a component downloads it, not only the component class itself. For +example, a route that imports `org.apache.camel.component.aws2.s3.AWS2S3Constants` for the +headers of the `aws-s3-source` Kamelet gets `camel-aws2-s3` before the route is compiled. A class +that is only looked up while running still needs the component class itself, so probing whether +a class is present does not download a component. + A dependency you declare yourself, with `--dep`, `camel.jbang.dependencies`, or in the project's POM, is on the classpath before any lookup, so it always takes precedence. Use that when a mapped library is not the one you want, for example the ActiveMQ 5 client where the mapping diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/JavaKnownImportsDownloader.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/JavaKnownImportsDownloader.java index 70bb7c32673e..9f7dffc9fd2b 100644 --- a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/JavaKnownImportsDownloader.java +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/JavaKnownImportsDownloader.java @@ -52,7 +52,7 @@ public class JavaKnownImportsDownloader implements CompilePreProcessor { List<String> imports = determineImports(code); for (String imp : imports) { // attempt known dependency resolver first - MavenGav gav = knownDependenciesResolver.mavenGavForClass(imp); + MavenGav gav = knownDependenciesResolver.mavenGavForImport(imp); if (gav != null) { downloadLoader(gav.getGroupId(), gav.getArtifactId(), gav.getVersion()); } else { diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/KnownDependenciesResolver.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/KnownDependenciesResolver.java index 79f8999be675..771274d557cf 100644 --- a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/KnownDependenciesResolver.java +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/KnownDependenciesResolver.java @@ -20,8 +20,10 @@ import java.io.InputStream; import java.net.URL; import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Properties; +import java.util.Set; import org.apache.camel.CamelContext; import org.apache.camel.tooling.maven.MavenGav; @@ -29,6 +31,8 @@ import org.apache.camel.tooling.maven.MavenGav; public final class KnownDependenciesResolver { private final Map<String, String> mappings = new HashMap<>(); + // the package of each component, only used for the imports of a source (see mavenGavForImport) + private final Map<String, String> componentPackages = new HashMap<>(); private final CamelContext camelContext; private final String springBootVersion; private final String quarkusVersion; @@ -40,17 +44,17 @@ public final class KnownDependenciesResolver { } public void loadKnownDependencies() { - doLoadKnownDependencies("camel-main-known-dependencies.properties"); - doLoadKnownDependencies("camel-component-known-dependencies.properties"); + doLoadKnownDependencies("camel-main-known-dependencies.properties", false); + doLoadKnownDependencies("camel-component-known-dependencies.properties", true); // third-party libraries mapped by package, generated from src/main/known-third-party-libraries.properties - doLoadKnownDependencies("camel-thirdparty-known-dependencies.properties"); + doLoadKnownDependencies("camel-thirdparty-known-dependencies.properties", false); } public void loadKnownFactoryFinderDependencies() { - doLoadKnownDependencies("camel-factoryfinder-known-dependencies.properties"); + doLoadKnownDependencies("camel-factoryfinder-known-dependencies.properties", false); } - private void doLoadKnownDependencies(String name) { + private void doLoadKnownDependencies(String name, boolean byPackage) { try { Enumeration<URL> resources = getClass().getClassLoader().getResources(name); while (resources.hasMoreElements()) { @@ -63,6 +67,9 @@ public final class KnownDependenciesResolver { String value = prop.getProperty(key); map.put(key, value); } + if (byPackage) { + addPackageMappings(map, componentPackages); + } addMappings(map); } } @@ -71,13 +78,55 @@ public final class KnownDependenciesResolver { } } + /** + * The package of each class, unless the package is shared by classes of different dependencies, or is a base + * package such as <tt>org.apache.camel</tt> that would match every Camel class. + */ + private static void addPackageMappings(Map<String, String> classes, Map<String, String> packages) { + Set<String> shared = new HashSet<>(); + for (Map.Entry<String, String> entry : classes.entrySet()) { + String key = entry.getKey(); + int pos = key.lastIndexOf('.'); + if (pos == -1) { + continue; + } + String pkg = key.substring(0, pos); + if (pkg.chars().filter(ch -> ch == '.').count() < 3) { + continue; + } + String existing = packages.putIfAbsent(pkg, entry.getValue()); + if (existing != null && !existing.equals(entry.getValue())) { + shared.add(pkg); + } + } + shared.forEach(packages::remove); + } + public void addMappings(Map<String, String> mappings) { this.mappings.putAll(mappings); } public MavenGav mavenGavForClass(String className) { + return toMavenGav(findGav(mappings, className)); + } + + /** + * The dependency of a class a source imports: as {@link #mavenGavForClass(String)}, and also any class of a Camel + * component (such as the constants of its headers), not only the component class itself. + * <p/> + * Only for imports: a class that is looked up at runtime is often only probed for, which must not download a + * component. + */ + public MavenGav mavenGavForImport(String className) { + String gav = findGav(mappings, className); + if (gav == null) { + gav = findGav(componentPackages, className); + } + return toMavenGav(gav); + } + + private MavenGav toMavenGav(String gav) { MavenGav answer = null; - String gav = findGav(className); if (gav != null) { answer = MavenGav.parseGav(gav, camelContext.getVersion()); } @@ -92,7 +141,7 @@ public final class KnownDependenciesResolver { return answer; } - private String findGav(String prefix) { + private static String findGav(Map<String, String> mappings, String prefix) { String gav = mappings.get(prefix); while (gav == null && prefix.lastIndexOf(".") != -1) { prefix = prefix.substring(0, prefix.lastIndexOf(".")); diff --git a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/KnownDependenciesResolverTest.java b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/KnownDependenciesResolverTest.java index 15f43fa63f58..90a9f752229f 100644 --- a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/KnownDependenciesResolverTest.java +++ b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/KnownDependenciesResolverTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; public class KnownDependenciesResolverTest { @@ -84,6 +85,39 @@ public class KnownDependenciesResolverTest { assertEquals(null, resolver.mavenGavForClass("org.apache.commons.Anything")); } + @Test + void anImportResolvesAnyClassOfAComponent() { + // CAMEL-25239: any class of a component a source imports, not only the component class itself, such as the + // constants of the headers of a component that a kamelet uses + KnownDependenciesResolver resolver = new KnownDependenciesResolver(new SimpleCamelContext(), null, null); + resolver.loadKnownDependencies(); + + assertImport(resolver, "org.apache.camel.component.aws2.s3.AWS2S3Component", "camel-aws2-s3"); + assertImport(resolver, "org.apache.camel.component.aws2.s3.AWS2S3Constants", "camel-aws2-s3"); + assertImport(resolver, "org.apache.camel.component.aws2.s3.utils.AWS2S3Utils", "camel-aws2-s3"); + // a sibling package of another component is not mistaken for it + assertImport(resolver, "org.apache.camel.component.aws2.s3vectors.AWS2S3VectorsConstants", "camel-aws2-s3-vectors"); + // a sub package of another component's package wins for its own classes + assertImport(resolver, "org.apache.camel.component.file.remote.SftpConstants", "camel-ftp"); + assertImport(resolver, "org.apache.camel.component.file.GenericFile", "camel-file"); + // the libraries still resolve as before + assertGav(resolver, "com.fasterxml.jackson.databind.ObjectMapper", "com.fasterxml.jackson.core", "jackson-databind"); + // a component in a base package does not claim every class in it + assertNull(resolver.mavenGavForImport("org.apache.camel.Exchange")); + assertNull(resolver.mavenGavForImport("org.apache.camel.component.Anything")); + // a class looked up at runtime (often only probed for) still needs the component class itself + assertNull(resolver.mavenGavForClass("org.apache.camel.component.aws2.s3.AWS2S3Constants")); + assertGav(resolver, "org.apache.camel.component.aws2.s3.AWS2S3Component", "org.apache.camel", "camel-aws2-s3"); + } + + private static void assertImport(KnownDependenciesResolver resolver, String className, String artifactId) { + MavenGav gav = resolver.mavenGavForImport(className); + assertNotNull(gav, className); + assertEquals("org.apache.camel", gav.getGroupId(), className); + assertEquals(artifactId, gav.getArtifactId(), className); + assertNotNull(gav.getVersion(), className + " version is null"); + } + private static void assertGav(KnownDependenciesResolver resolver, String className, String groupId, String artifactId) { MavenGav gav = resolver.mavenGavForClass(className); assertNotNull(gav, className);
