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 1386c26055cc CAMEL-24843: a Groovy script's imports go through the 
compile pre-processors, so the Camel CLI downloads a known library; an 
unresolved class says what to do; the validator checks the imports (#26623)
1386c26055cc is described below

commit 1386c26055cccc68ba8b85faaa83a779e9656231
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 20 12:42:39 2026 +0200

    CAMEL-24843: a Groovy script's imports go through the compile 
pre-processors, so the Camel CLI downloads a known library; an unresolved class 
says what to do; the validator checks the imports (#26623)
    
    camel-groovy runs the registered CompilePreProcessors on a script before 
compiling it, as the Java DSL does, and an unresolved class says the class is 
not on the classpath and how a dependency is declared. camel-kamelet-main's 
import scanner accepts Groovy imports, and commons-validator joins the known 
third-party list with the parent's version property. camel validate reports an 
import of an unknown library and names a known one with its pom dependency for 
a Maven runtime. Found in t [...]
---
 .../groovy/DefaultGroovyScriptCompiler.java        |  12 +-
 .../camel/language/groovy/GroovyExpression.java    |  10 +-
 .../camel/language/groovy/GroovyLanguage.java      |  46 ++++++-
 .../groovy/GroovyCompilePreProcessorTest.java      |  80 ++++++++++++
 .../jbang/core/commands/ai/GroovyImportChecks.java | 136 +++++++++++++++++++++
 .../jbang/core/commands/ai/SourceValidator.java    |   4 +-
 .../core/commands/ai/GroovyImportChecksTest.java   | 100 +++++++++++++++
 .../main/download/JavaKnownImportsDownloader.java  |  15 ++-
 .../main/known-third-party-libraries.properties    |   1 +
 .../download/JavaKnownImportsDownloaderTest.java   |  45 +++++++
 10 files changed, 439 insertions(+), 10 deletions(-)

diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
index d87fa83d107d..9018cb75c97a 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
@@ -47,6 +47,7 @@ import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
+import org.codehaus.groovy.control.CompilationFailedException;
 import org.codehaus.groovy.control.CompilerConfiguration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -333,12 +334,19 @@ public class DefaultGroovyScriptCompiler extends 
ServiceSupport
         GroovyShell shell = new GroovyShell(cl, cc);
 
         // parse code into classes and add to classloader
-        for (String code : codes.values()) {
+        for (Map.Entry<String, String> entry : codes.entrySet()) {
+            String code = entry.getValue();
             if (LOG.isTraceEnabled()) {
                 LOG.trace("Compiling Groovy source:\n{}", code);
             }
             counter++;
-            Class<?> clazz = shell.getClassLoader().parseClass(code);
+            GroovyLanguage.preCompile(camelContext, entry.getKey(), code);
+            Class<?> clazz;
+            try {
+                clazz = shell.getClassLoader().parseClass(code);
+            } catch (CompilationFailedException e) {
+                throw GroovyLanguage.compileFailure(e);
+            }
             if (clazz != null) {
                 String name = clazz.getName();
                 LOG.debug("Compiled Groovy class: {}", name);
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
index 0bde520fe6a1..11b7c89fddbe 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
@@ -37,6 +37,7 @@ import org.apache.camel.attachment.DefaultAttachmentMessage;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.ExpressionSupport;
 import org.apache.camel.support.LanguageHelper;
+import org.codehaus.groovy.control.CompilationFailedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -130,8 +131,13 @@ public class GroovyExpression extends ExpressionSupport {
                         = 
exchange.getContext().getCamelContextExtension().getContextPlugin(GroovyScriptClassLoader.class);
                 GroovyShell shell = shellFactory != null ? 
shellFactory.createGroovyShell(exchange)
                         : cl != null ? new GroovyShell(cl) : new GroovyShell();
-                return name != null
-                        ? shell.getClassLoader().parseClass(text, name) : 
shell.getClassLoader().parseClass(text);
+                GroovyLanguage.preCompile(exchange.getContext(), name, text);
+                try {
+                    return name != null
+                            ? shell.getClassLoader().parseClass(text, name) : 
shell.getClassLoader().parseClass(text);
+                } catch (CompilationFailedException e) {
+                    throw GroovyLanguage.compileFailure(e);
+                }
             });
             c = new CompiledScript(r.context, r.language, generation, 
fileName, scriptClass, constructor(scriptClass));
             compiled = c;
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
index 05ddabd9c5c5..0eadc9bd5445 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
@@ -27,10 +27,13 @@ import java.util.function.Supplier;
 import groovy.lang.Binding;
 import groovy.lang.GroovyShell;
 import groovy.lang.Script;
+import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
 import org.apache.camel.Ordered;
+import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.Service;
 import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.CompilePreProcessor;
 import org.apache.camel.spi.EventNotifier;
 import org.apache.camel.spi.ScriptingLanguage;
 import org.apache.camel.spi.annotations.Language;
@@ -40,6 +43,7 @@ import org.apache.camel.support.ObjectHelper;
 import org.apache.camel.support.SimpleEventNotifierSupport;
 import org.apache.camel.support.TypedLanguageSupport;
 import org.apache.camel.support.service.ServiceHelper;
+import org.codehaus.groovy.control.CompilationFailedException;
 import org.codehaus.groovy.runtime.InvokerHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -178,7 +182,12 @@ public class GroovyLanguage extends TypedLanguageSupport 
implements ScriptingLan
             // prefer to use classloader from groovy script compiler, and if 
not fallback to app context
             ClassLoader cl = 
getCamelContext().getCamelContextExtension().getContextPlugin(GroovyScriptClassLoader.class);
             GroovyShell shell = cl != null ? new GroovyShell(cl) : new 
GroovyShell();
-            return shell.getClassLoader().parseClass(text);
+            preCompile(getCamelContext(), null, text);
+            try {
+                return shell.getClassLoader().parseClass(text);
+            } catch (CompilationFailedException e) {
+                throw compileFailure(e);
+            }
         });
         Script gs = ObjectHelper.newInstance(clazz, Script.class);
         if (bindings != null) {
@@ -262,4 +271,39 @@ public class GroovyLanguage extends TypedLanguageSupport 
implements ScriptingLan
             return new GroovyLanguage(cache, false);
         }
     }
+
+    /**
+     * Runs the registered {@link CompilePreProcessor}s on the script before 
it is compiled, as the Java DSL does for
+     * its sources: with the Camel CLI that downloads the known library of an 
import (CAMEL-24843).
+     */
+    static void preCompile(CamelContext context, String name, String code) {
+        for (CompilePreProcessor pre : 
context.getRegistry().findByType(CompilePreProcessor.class)) {
+            try {
+                pre.preCompile(context, name, code);
+            } catch (Exception e) {
+                throw RuntimeCamelException.wrapRuntimeException(e);
+            }
+        }
+    }
+
+    /**
+     * A compilation failure whose cause is a class the script imports and the 
classpath does not have: the compiler
+     * names the class and nothing else; the message adds what to do, without 
knowing the runtime (CAMEL-24843).
+     */
+    static RuntimeException compileFailure(CompilationFailedException e) {
+        String msg = e.getMessage();
+        if (msg != null && msg.contains("unable to resolve class")) {
+            String cls = msg.substring(msg.indexOf("unable to resolve class") 
+ "unable to resolve class".length()).trim();
+            int end = cls.indexOf('\n');
+            cls = (end > 0 ? cls.substring(0, end) : cls).trim();
+            return new RuntimeCamelException(
+                    "Groovy cannot resolve the class " + cls + ": it is not on 
the classpath. Add the library that"
+                                             + " provides it as a dependency 
of the application (a Maven dependency;"
+                                             + " with the Camel CLI 
camel.jbang.dependencies=groupId:artifactId:version"
+                                             + " in application.properties, or 
a //DEPS line); a class of your own"
+                                             + " goes in a .groovy or .java 
file next to the route.",
+                    e);
+        }
+        return e;
+    }
 }
diff --git 
a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyCompilePreProcessorTest.java
 
b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyCompilePreProcessorTest.java
new file mode 100644
index 000000000000..bac1a4febc84
--- /dev/null
+++ 
b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyCompilePreProcessorTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.language.groovy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.CompilePreProcessor;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The registered compile pre-processors see a Groovy script before it is 
compiled, as they see a Java source
+ * (CAMEL-24843): with the Camel CLI that is what downloads the library of an 
import.
+ */
+public class GroovyCompilePreProcessorTest extends CamelTestSupport {
+
+    private final List<String> seen = new ArrayList<>();
+
+    @Override
+    protected void bindToRegistry(org.apache.camel.spi.Registry registry) {
+        registry.bind("myPreProcessor",
+                (CompilePreProcessor) (CamelContext camelContext, String name, 
String code) -> seen.add(code));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setBody().groovy("import 
java.util.Locale\nbody.toUpperCase(Locale.ROOT)")
+                        .to("mock:result");
+            }
+        };
+    }
+
+    @Test
+    public void testPreProcessorSeesTheScript() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("HELLO");
+        template.sendBody("direct:start", "hello");
+        MockEndpoint.assertIsSatisfied(context);
+
+        assertEquals(1, seen.size(), seen.toString());
+        assertTrue(seen.get(0).contains("import java.util.Locale"), 
seen.get(0));
+    }
+
+    @Test
+    public void testUnresolvedClassSaysWhatToDo() {
+        GroovyLanguage groovy = (GroovyLanguage) 
context.resolveLanguage("groovy");
+        Exception e = assertThrows(Exception.class,
+                () -> groovy.evaluate("import 
org.example.missing.EmailValidator\nEmailValidator.getInstance()", null,
+                        Object.class));
+        String msg = e.getMessage();
+        assertTrue(msg.contains("Groovy cannot resolve the class 
org.example.missing.EmailValidator"), msg);
+        
assertTrue(msg.contains("camel.jbang.dependencies=groupId:artifactId:version"), 
msg);
+        assertTrue(msg.contains("Maven dependency"), msg);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/GroovyImportChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/GroovyImportChecks.java
new file mode 100644
index 000000000000..5f2369cebc0f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/GroovyImportChecks.java
@@ -0,0 +1,136 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static 
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.countLeadingSpaces;
+
+/**
+ * Checks on the imports of the Groovy expressions of a YAML route 
(CAMEL-24843). A Groovy script compiles against the
+ * application's classpath: an import of a library the application does not 
declare fails at runtime with "unable to
+ * resolve class", which names the class and nothing else. The check names the 
library when it is a known one and says
+ * how to declare it for the runtime; with the Camel CLI a known library is 
downloaded when the script is compiled, so
+ * only an unknown one is reported there.
+ */
+public final class GroovyImportChecks {
+
+    private static final Pattern IMPORT
+            = 
Pattern.compile("^\\s*import\\s+(static\\s+)?([a-zA-Z][.\\w]*(?:\\.\\*)?)\\s*;?\\s*$");
+    /** Packages every application has: the JDK, Groovy and Camel itself. */
+    private static final List<String> ALWAYS_PRESENT
+            = List.of("java.", "javax.", "jakarta.", "groovy.", 
"org.codehaus.groovy.", "org.apache.camel.");
+
+    private GroovyImportChecks() {
+    }
+
+    /**
+     * @param  content      the YAML route file
+     * @param  runtime      null or "jbang" for the Camel CLI (a known library 
is downloaded when the script is
+     *                      compiled), "main", "spring-boot" or "quarkus" for 
a Maven project (the library must be
+     *                      declared in the pom)
+     * @param  projectTypes the classes the project declares itself (simple 
name to fully qualified name), from the Java
+     *                      and Groovy files next to the route
+     * @return              the messages, one per import to act on, empty when 
there is nothing to say
+     */
+    public static List<String> validateYamlGroovyImports(String content, 
String runtime, Map<String, String> projectTypes) {
+        List<String> errors = new ArrayList<>();
+        if (content == null || content.isBlank()) {
+            return errors;
+        }
+        boolean maven = runtime != null && !runtime.isBlank() && 
!"jbang".equalsIgnoreCase(runtime);
+        Set<String> projectClasses = new LinkedHashSet<>(projectTypes != null 
? projectTypes.values() : Set.of());
+        String[] lines = content.split("\n", -1);
+        for (int i = 0; i < lines.length; i++) {
+            String trimmed = lines[i].trim();
+            String key = trimmed.startsWith("- ") ? trimmed.substring(2) : 
trimmed;
+            if (!key.startsWith("groovy:")) {
+                continue;
+            }
+            // the script: a block scalar under groovy: |, the lines under 
groovy: / expression: |, or the inline value
+            int indent = countLeadingSpaces(lines[i]);
+            List<int[]> scriptLines = new ArrayList<>();
+            String value = key.substring("groovy:".length()).trim();
+            if (value.isEmpty() || value.startsWith("|") || 
value.startsWith(">")) {
+                for (int j = i + 1; j < lines.length; j++) {
+                    if (lines[j].isBlank()) {
+                        continue;
+                    }
+                    if (countLeadingSpaces(lines[j]) <= indent) {
+                        break;
+                    }
+                    scriptLines.add(new int[] { j });
+                }
+            } else {
+                scriptLines.add(new int[] { i });
+            }
+            for (int[] sl : scriptLines) {
+                String line = lines[sl[0]].trim();
+                if (line.startsWith("expression:")) {
+                    // the canonical form: groovy: on its own line, 
expression: | below it, the script under that;
+                    // the expression: line is collected with the script lines 
and is not one of them
+                    continue;
+                }
+                Matcher m = IMPORT.matcher(line);
+                if (!m.find()) {
+                    continue;
+                }
+                String cls = m.group(2);
+                if (m.group(1) != null && cls.contains(".")) {
+                    cls = cls.substring(0, cls.lastIndexOf('.'));
+                }
+                if (cls.endsWith(".*")) {
+                    cls = cls.substring(0, cls.length() - 2);
+                }
+                final String fqcn = cls;
+                if (ALWAYS_PRESENT.stream().anyMatch(fqcn::startsWith) || 
projectClasses.contains(fqcn)
+                        || projectClasses.stream().anyMatch(c -> 
c.endsWith("." + fqcn))) {
+                    continue;
+                }
+                String known = BeanRefChecks.knownDependency(fqcn);
+                int lineNum = sl[0] + 1;
+                if (known != null && known.startsWith("camel:")) {
+                    continue;
+                }
+                if (known != null) {
+                    if (maven) {
+                        String[] gav = known.split(":");
+                        errors.add("Line " + lineNum + ": the Groovy import " 
+ fqcn + " is a class of " + known
+                                   + ": the application must declare it as a 
dependency in the pom (<dependency>"
+                                   + "<groupId>" + gav[0] + 
"</groupId><artifactId>" + gav[1] + "</artifactId>"
+                                   + (gav.length > 2 && 
!gav[2].startsWith("${") ? "<version>" + gav[2] + "</version>" : "")
+                                   + "</dependency>)");
+                    }
+                    // the Camel CLI downloads a known library when the script 
is compiled
+                    continue;
+                }
+                errors.add("Line " + lineNum + ": the Groovy import " + fqcn + 
" is not a class of the JDK, Groovy, Camel"
+                           + " or this project, and not a known library: 
declare the library that provides it as a"
+                           + " dependency (with the Camel CLI 
camel.jbang.dependencies=groupId:artifactId:version in"
+                           + " application.properties or a //DEPS line; in a 
Maven project a pom dependency), or put"
+                           + " the class in a .groovy or .java file next to 
the route");
+            }
+        }
+        return errors;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 9d7afd0fe5ff..6312cee39891 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -107,8 +107,10 @@ public final class SourceValidator {
             List<String> msgs = validateCamelYaml(content, catalog, 
schemaValidator);
             if (directory != null && msgs.isEmpty()) {
                 msgs = new ArrayList<>(msgs);
-                msgs.addAll(validateYamlBeanRefs(content, 
BeanDeclarations.scan(directory, fileName), catalog));
+                BeanDeclarations declarations = 
BeanDeclarations.scan(directory, fileName);
+                msgs.addAll(validateYamlBeanRefs(content, declarations, 
catalog));
                 msgs.addAll(validateResourceRefs(content, directory));
+                
msgs.addAll(GroovyImportChecks.validateYamlGroovyImports(content, null, 
declarations.javaClasses()));
             }
             return msgs;
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/GroovyImportChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/GroovyImportChecksTest.java
new file mode 100644
index 000000000000..e5a0b9446a8a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/GroovyImportChecksTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GroovyImportChecksTest {
+
+    private static final String YAML = """
+            - route:
+                from:
+                  uri: "timer:t?repeatCount=1"
+                  steps:
+                    - choice:
+                        when:
+                          - expression:
+                              groovy: |
+                                import 
org.apache.commons.validator.routines.EmailValidator
+                                import java.util.Locale
+                                import com.example.Unknown
+                                import org.apache.camel.Exchange
+                                import com.example.MyBean
+                                
EmailValidator.getInstance().isValid(body.email)
+                            steps:
+                              - log: "ok"
+            """;
+
+    @Test
+    void cliDownloadsAKnownLibraryAndReportsAnUnknownOne() {
+        List<String> errors = 
GroovyImportChecks.validateYamlGroovyImports(YAML, null, Map.of("MyBean", 
"com.example.MyBean"));
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 11: the Groovy import 
com.example.Unknown is not a class of the JDK")
+                
.contains("camel.jbang.dependencies=groupId:artifactId:version");
+    }
+
+    @Test
+    void mavenRuntimeNamesTheKnownLibrary() {
+        List<String> errors
+                = GroovyImportChecks.validateYamlGroovyImports(YAML, 
"spring-boot", Map.of("MyBean", "com.example.MyBean"));
+        assertThat(errors).hasSize(2);
+        assertThat(errors.get(0)).startsWith(
+                "Line 9: the Groovy import 
org.apache.commons.validator.routines.EmailValidator is a class of 
commons-validator:commons-validator:")
+                .contains("<artifactId>commons-validator</artifactId>");
+        assertThat(errors.get(1)).startsWith("Line 11: ");
+    }
+
+    @Test
+    void canonicalFormWithExpressionBlock() {
+        // groovy: on its own line and the script under expression: | (what 
camel validate normalize writes)
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - setBody:
+                            expression:
+                              groovy:
+                                expression: |
+                                  import com.example.Unknown
+                                  import 
org.apache.commons.validator.routines.EmailValidator
+                                  Unknown.of(body)
+                """;
+        List<String> errors = 
GroovyImportChecks.validateYamlGroovyImports(yaml, null, Map.of());
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 9: the Groovy import 
com.example.Unknown");
+        assertThat(GroovyImportChecks.validateYamlGroovyImports(yaml, "main", 
Map.of())).hasSize(2);
+    }
+
+    @Test
+    void inlineExpressionAndNoImports() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - setBody:
+                            groovy: "body.toUpperCase()"
+                """;
+        assertThat(GroovyImportChecks.validateYamlGroovyImports(yaml, null, 
Map.of())).isEmpty();
+    }
+}
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 d2a949333f29..70bb7c32673e 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
@@ -33,8 +33,9 @@ import org.apache.camel.tooling.model.PojoBeanModel;
  */
 public class JavaKnownImportsDownloader implements CompilePreProcessor {
 
+    // Java and Groovy: import a.b.C; import a.b.C (no semicolon in Groovy); 
import static a.b.C.member
     private static final Pattern IMPORT_PATTERN = Pattern.compile(
-            "^import\\s+([a-zA-Z][.\\w]*)\\s*;", Pattern.MULTILINE);
+            
"^\\s*import\\s+(static\\s+)?([a-zA-Z][.\\w]*(?:\\.\\*)?)\\s*;?\\s*$", 
Pattern.MULTILINE);
 
     private final CamelCatalog catalog = new DefaultCamelCatalog();
     private final DependencyDownloader downloader;
@@ -73,12 +74,18 @@ public class JavaKnownImportsDownloader implements 
CompilePreProcessor {
         }
     }
 
-    private static List<String> determineImports(String content) {
+    static List<String> determineImports(String content) {
         List<String> answer = new ArrayList<>();
         final Matcher matcher = IMPORT_PATTERN.matcher(content);
         while (matcher.find()) {
-            String imp = matcher.group(1);
-            imp = imp.trim();
+            String imp = matcher.group(2).trim();
+            if (matcher.group(1) != null && imp.contains(".")) {
+                // import static a.b.C.member: the class is a.b.C
+                imp = imp.substring(0, imp.lastIndexOf('.'));
+            }
+            if (imp.endsWith(".*")) {
+                imp = imp.substring(0, imp.length() - 2);
+            }
             answer.add(imp);
         }
         return answer;
diff --git 
a/dsl/camel-kamelet-main/src/main/known-third-party-libraries.properties 
b/dsl/camel-kamelet-main/src/main/known-third-party-libraries.properties
index 9acfe5eaa64c..7828178f03e7 100644
--- a/dsl/camel-kamelet-main/src/main/known-third-party-libraries.properties
+++ b/dsl/camel-kamelet-main/src/main/known-third-party-libraries.properties
@@ -163,6 +163,7 @@ org.xmlunit = org.xmlunit:xmlunit-core:${xmlunit-version}
 # Commons and utilities
 org.apache.commons.lang3 = 
org.apache.commons:commons-lang3:${commons-lang3-version}
 org.apache.commons.io = commons-io:commons-io:${commons-io-version}
+org.apache.commons.validator = 
commons-validator:commons-validator:${commons-validator-version}
 com.google.common = com.google.guava:guava:${guava-version}
 org.jsoup = org.jsoup:jsoup:${jsoup-version}
 
diff --git 
a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/JavaKnownImportsDownloaderTest.java
 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/JavaKnownImportsDownloaderTest.java
new file mode 100644
index 000000000000..77001cdc99c6
--- /dev/null
+++ 
b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/JavaKnownImportsDownloaderTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class JavaKnownImportsDownloaderTest {
+
+    @Test
+    public void testJavaAndGroovyImports() {
+        // Java with semicolons, Groovy without, static imports and wildcards 
(CAMEL-24843)
+        List<String> imports = JavaKnownImportsDownloader.determineImports("""
+                package com.example;
+
+                import org.apache.commons.validator.routines.EmailValidator;
+                import org.apache.commons.lang3.StringUtils
+                import static 
org.apache.commons.text.StringEscapeUtils.escapeJson;
+                import static java.time.Duration.ofSeconds
+                import com.fasterxml.jackson.databind.*
+
+                def x = 1
+                """);
+        
assertEquals(List.of("org.apache.commons.validator.routines.EmailValidator", 
"org.apache.commons.lang3.StringUtils",
+                "org.apache.commons.text.StringEscapeUtils", 
"java.time.Duration", "com.fasterxml.jackson.databind"),
+                imports);
+    }
+}

Reply via email to