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 d8befc8825c3 CAMEL-24853: camel export keeps a Groovy script the
routes reference as a resource: at the resources root
d8befc8825c3 is described below
commit d8befc8825c3085a9490cdb149db952e83f6c50f
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 20 20:42:01 2026 +0200
CAMEL-24853: camel export keeps a Groovy script the routes reference as a
resource: at the resources root
camel export copied every .groovy file into src/main/resources/camel-groovy,
the directory camel.main.groovyScriptPattern compiles scripts from at
startup. A Groovy mapping script that a route uses as the body of an
expression (groovy: {expression:
"resource:classpath:shipment-mapping.groovy"})
was then out of reach of the reference in the exported project, and compiled
as a script it is not.
The export scans the route files (camel.main.routesIncludePattern, file: and
classpath: entries) for resource:classpath:x / resource:file:x references
and
keeps a referenced Groovy file at the resources root, where the reference
resolves on Spring Boot, Quarkus and camel-main alike; a file matches by its
path, a path suffix, or its bare name when that is how it was referenced.
Groovy files the routes do not reference go to camel-groovy as before.
Upgrade guide note added.
Closes #26635
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 5 ++
.../dsl/jbang/core/commands/ExportBaseCommand.java | 65 ++++++++++++++++++++++
.../camel/dsl/jbang/core/commands/ExportTest.java | 35 ++++++++++++
.../test/resources/groovy-resource-demo.camel.yaml | 30 ++++++++++
.../src/test/resources/shipment-mapping.groovy | 18 ++++++
5 files changed, 153 insertions(+)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index a5b9bd21be8f..e7d5ca92879a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -796,6 +796,11 @@ The compact notation they used to be written in (`setBody:
{simple: "..."}`, `lo
rewrites a file in the canonical format. Existing files keep working; only the
generated starting point
changed. Scripts that post-process the generated YAML by matching the old text
need to be updated.
+`camel export` keeps a Groovy script that a route references as a resource
(`resource:classpath:mapping.groovy`
+or `resource:file:mapping.groovy`, a mapping script used as the body of an
expression) at the resources root, where
+the reference resolves in the exported project. Other Groovy files go to
`src/main/resources/camel-groovy` as before,
+where they are compiled as scripts at startup.
+
=== camel-jbang (MCP servers)
The Camel authoring tools for AI agents are now defined once, in
`camel-jbang-core`, and exposed under the
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
index cf069a12cbab..e52c2ff24a9b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java
@@ -881,6 +881,64 @@ public abstract class ExportBaseCommand extends
CamelCommand {
}
}
+ /** resource:classpath:x or resource:file:x in a route file: the file x is
referenced by its path. */
+ private static final Pattern RESOURCE_REF_PATTERN =
Pattern.compile("resource:(?:classpath|file):([^\"'\\s?&,]+)");
+
+ /**
+ * The paths of the files the route files
(camel.main.routesIncludePattern) reference as resource:classpath: or
+ * resource:file:, as written in the reference (a relative path, or a bare
name), so the export can keep them where
+ * the reference resolves. A file: route is read from the file system, a
classpath: route from the classpath.
+ */
+ private static Set<String> resourceReferencedFiles(String routeFiles) {
+ Set<String> paths = new HashSet<>();
+ if (routeFiles == null || routeFiles.isBlank()) {
+ return paths;
+ }
+ for (String f : routeFiles.split(",")) {
+ f = f.trim();
+ String scheme = getScheme(f);
+ if (scheme != null) {
+ f = f.substring(scheme.length() + 1);
+ }
+ String content = null;
+ try {
+ if ("classpath".equals(scheme)) {
+ try (InputStream is =
ExportBaseCommand.class.getClassLoader().getResourceAsStream(f)) {
+ content = is != null ? new String(is.readAllBytes(),
StandardCharsets.UTF_8) : null;
+ }
+ } else if (scheme == null || "file".equals(scheme)) {
+ Path path = Paths.get(f);
+ content = Files.isRegularFile(path) ?
Files.readString(path) : null;
+ }
+ } catch (IOException e) {
+ // ignore: the file is copied as is
+ }
+ if (content == null) {
+ continue;
+ }
+ Matcher m = RESOURCE_REF_PATTERN.matcher(content);
+ while (m.find()) {
+ String ref = m.group(1);
+ if (ref.startsWith("./")) {
+ ref = ref.substring(2);
+ }
+ paths.add(ref);
+ }
+ }
+ return paths;
+ }
+
+ /** Whether the file is one the routes reference as a resource: by its
path, or by its name alone. */
+ private static boolean isResourceReferenced(Set<String> referenced, String
file) {
+ String name = FileUtil.stripPath(file);
+ for (String ref : referenced) {
+ if (file.equals(ref) || file.endsWith("/" + ref) ||
name.equals(ref)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
protected void copySourceFiles(
Path settings, Path profile, Path srcJavaDirRoot, Path srcJavaDir,
Path srcResourcesDir, Path srcCamelResourcesDir,
Path srcKameletsResourcesDir, String packageName)
@@ -897,6 +955,11 @@ public abstract class ExportBaseCommand extends
CamelCommand {
localKameletDir = localKameletDir.substring(scheme.length() +
1);
}
}
+ // the files the routes reference as resource:classpath:x or
resource:file:x (a Groovy mapping script used as an
+ // expression): they stay at the resources root, where the reference
resolves, instead of camel-groovy where
+ // they would be compiled as scripts and be out of reach of the
reference (CAMEL-24853)
+ Set<String> resourceReferenced =
resourceReferencedFiles(prop.getProperty("camel.main.routesIncludePattern"));
+
for (String k : SETTINGS_PROP_SOURCE_KEYS) {
String files;
if ("kamelet".equals(k)) {
@@ -944,6 +1007,8 @@ public abstract class ExportBaseCommand extends
CamelCommand {
targetDir = srcKameletsResourcesDir;
} else if (script) {
targetDir =
srcJavaDirRoot.getParent().resolve("scripts");
+ } else if (groovy &&
isResourceReferenced(resourceReferenced, f)) {
+ targetDir = srcResourcesDir;
} else if (groovy) {
targetDir = srcResourcesDir.resolve("camel-groovy");
} else if (tls) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java
index 0596df1a9c49..b77b8ee068f6 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportTest.java
@@ -841,6 +841,41 @@ class ExportTest {
Assertions.assertTrue(f.exists());
}
+ /** CAMEL-24853: a Groovy script the route references as a resource: stays
where the reference resolves. */
+ @ParameterizedTest
+ @MethodSource("runtimeProvider")
+ public void
shouldExportAResourceReferencedGroovyScriptToTheResourcesRoot(RuntimeType rt)
throws Exception {
+ Export command = createCommand(rt,
+ new String[] {
+ "src/test/resources/groovy-resource-demo.camel.yaml",
"src/test/resources/shipment-mapping.groovy",
+ "src/test/resources/demo.groovy" },
+ "--gav=examples:route:1.0.0", "--dir=" + workingDir,
"--quiet");
+ Assertions.assertEquals(0, command.doCall());
+
+ File referenced =
workingDir.toPath().resolve("src/main/resources/shipment-mapping.groovy").toFile();
+ Assertions.assertTrue(referenced.isFile(), "the referenced script is
at the resources root");
+
Assertions.assertFalse(workingDir.toPath().resolve("src/main/resources/camel-groovy/shipment-mapping.groovy").toFile()
+ .exists(), "and not in camel-groovy, where it would be
compiled as a script");
+ File script =
workingDir.toPath().resolve("src/main/resources/camel-groovy/demo.groovy").toFile();
+ Assertions.assertTrue(script.isFile(),
+ "a script the routes do not reference as a resource goes to
camel-groovy as before");
+ }
+
+ /** CAMEL-24853: the same for a route given as a classpath: resource, read
from the classpath for the scan. */
+ @Test
+ public void shouldExportAResourceReferencedGroovyScriptOfAClasspathRoute()
throws Exception {
+ Export command = createCommand(RuntimeType.main,
+ new String[] {
+ "classpath:groovy-resource-demo.camel.yaml",
"src/test/resources/shipment-mapping.groovy",
+ "src/test/resources/demo.groovy" },
+ "--gav=examples:route:1.0.0", "--dir=" + workingDir,
"--quiet");
+ Assertions.assertEquals(0, command.doCall());
+
+
Assertions.assertTrue(workingDir.toPath().resolve("src/main/resources/shipment-mapping.groovy").toFile().isFile(),
+ "the script the classpath: route references is at the
resources root");
+
Assertions.assertTrue(workingDir.toPath().resolve("src/main/resources/camel-groovy/demo.groovy").toFile().isFile());
+ }
+
@Test
public void shouldExportGenAiRouteWithObservability() throws Exception {
Export command = new Export(new CamelJBangMain());
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/groovy-resource-demo.camel.yaml
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/groovy-resource-demo.camel.yaml
new file mode 100644
index 000000000000..bd616fb560cd
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/groovy-resource-demo.camel.yaml
@@ -0,0 +1,30 @@
+#
+# 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.
+#
+
+- route:
+ from:
+ uri: timer
+ parameters:
+ timerName: demo
+ repeatCount: "1"
+ steps:
+ - setBody:
+ expression:
+ groovy:
+ expression: "resource:classpath:shipment-mapping.groovy"
+ - log:
+ message: ${body}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/shipment-mapping.groovy
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/shipment-mapping.groovy
new file mode 100644
index 000000000000..b4eaaa14e668
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/shipment-mapping.groovy
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+[shipmentRef: 'SHIP-1']