You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/20 18:42:08 UTC

(camel) branch main updated: CAMEL-24853: camel export keeps a Groovy script the routes reference as a resource: at the resources root

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 <cl...@gmail.com>
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 <no...@anthropic.com>
    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']