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