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']

Reply via email to