This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch gs
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 376face1dfe73b683879cab2036767c0ee17b3d9
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 14 09:25:32 2025 +0200

    CAMEL-22214: camel-groovy - Allow to pre-load groovy source files for 
shared functions and DTOs
---
 .../camel/language/groovy/GroovyExpression.java    |   3 +-
 .../camel/language/groovy/GroovyLanguage.java      |   5 +-
 .../language/groovy/GroovyScriptClassLoader.java   |  55 ++++++++++
 .../language/groovy/GroovyScriptCompiler.java      | 114 +++++++++++++++++++++
 .../processor/groovy/GroovyCompilerRouteTest.java  |  64 ++++++++++++
 .../camel/processor/groovy/GroovyCompilerTest.java |  45 ++++++++
 .../src/test/resources/myscript/Cheese.groovy      |  25 +++++
 .../src/test/resources/myscript/Dude.groovy        |  26 +++++
 8 files changed, 334 insertions(+), 3 deletions(-)

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 f9c7a38a228..bdcf49f8e05 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
@@ -79,7 +79,8 @@ public class GroovyExpression extends ExpressionSupport {
         final String key = fileName != null ? fileName + text : text;
         Class<Script> scriptClass = language.getScriptFromCache(key);
         if (scriptClass == null) {
-            ClassLoader cl = 
exchange.getContext().getApplicationContextClassLoader();
+            // prefer to use classloader from groovy script compiler, and if 
not fallback to app context
+            ClassLoader cl = 
exchange.getContext().getCamelContextExtension().getContextPlugin(GroovyScriptClassLoader.class);
             GroovyShell shell = shellFactory != null ? 
shellFactory.createGroovyShell(exchange)
                     : cl != null ? new GroovyShell(cl) : new GroovyShell();
             scriptClass = fileName != null
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 8e1faa20ff9..9160d612f79 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
@@ -143,8 +143,9 @@ public class GroovyLanguage extends TypedLanguageSupport 
implements ScriptingLan
         }
         Class<Script> clazz = getScriptFromCache(script);
         if (clazz == null) {
-            ClassLoader cl = 
getCamelContext().getApplicationContextClassLoader();
-            GroovyShell shell = new GroovyShell(cl);
+            // 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();
             clazz = shell.getClassLoader().parseClass(script);
             addScriptToCache(script, clazz);
         }
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyScriptClassLoader.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyScriptClassLoader.java
new file mode 100644
index 00000000000..330af11659d
--- /dev/null
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyScriptClassLoader.java
@@ -0,0 +1,55 @@
+/*
+ * 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.io.Closeable;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.codehaus.groovy.runtime.InvokerHelper;
+
+public class GroovyScriptClassLoader extends ClassLoader implements Closeable {
+
+    private final Map<String, Class<?>> classes = new HashMap<>();
+
+    public GroovyScriptClassLoader(ClassLoader parent) {
+        super(parent);
+    }
+
+    public void addClass(String name, Class<?> clazz) {
+        classes.put(name, clazz);
+    }
+
+    @Override
+    protected Class<?> findClass(String name) throws ClassNotFoundException {
+        Class<?> answer = classes.get(name);
+        if (answer == null) {
+            answer = super.findClass(name);
+        }
+        return answer;
+    }
+
+    @Override
+    public void close() throws IOException {
+        for (Class<?> clazz : classes.values()) {
+            InvokerHelper.removeClass(clazz);
+        }
+        classes.clear();
+    }
+
+}
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyScriptCompiler.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyScriptCompiler.java
new file mode 100644
index 00000000000..56c1b6190b4
--- /dev/null
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyScriptCompiler.java
@@ -0,0 +1,114 @@
+/*
+ * 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.io.File;
+import java.io.FileInputStream;
+import java.util.List;
+
+import groovy.lang.GroovyShell;
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.IOHelper;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GroovyScriptCompiler extends ServiceSupport implements 
CamelContextAware {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GroovyScriptCompiler.class);
+
+    private GroovyScriptClassLoader classLoader;
+    private CamelContext camelContext;
+    private String folder;
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    public String getFolder() {
+        return folder;
+    }
+
+    public void setFolder(String folder) {
+        this.folder = folder;
+    }
+
+    /**
+     * Loads the given class
+     *
+     * @param  name                   the FQN class name
+     * @return                        the loaded class
+     * @throws ClassNotFoundException is thrown if class is not found
+     */
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        if (classLoader != null) {
+            return classLoader.findClass(name);
+        } else {
+            throw new ClassNotFoundException(name);
+        }
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+
+        if (folder != null) {
+            LOG.info("Pre compiling groovy scripts from: {}", folder);
+
+            ClassLoader cl = camelContext.getApplicationContextClassLoader();
+            if (cl == null) {
+                cl = GroovyShell.class.getClassLoader();
+            }
+            classLoader = new GroovyScriptClassLoader(cl);
+            camelContext.getClassResolver().addClassLoader(classLoader);
+
+            // make classloader available for groovy language
+            
camelContext.getCamelContextExtension().addContextPlugin(GroovyScriptClassLoader.class,
 classLoader);
+
+            CompilerConfiguration cc = new CompilerConfiguration();
+            cc.setClasspathList(List.of(folder));
+
+            GroovyShell shell = new GroovyShell(cl, cc);
+
+            // discover each class from the folder
+            File[] files = new File(folder).listFiles();
+            if (files != null) {
+                for (File f : files) {
+                    String code = IOHelper.loadText(new FileInputStream(f));
+                    Class<?> clazz = shell.getClassLoader().parseClass(code);
+                    if (clazz != null) {
+                        classLoader.addClass(clazz.getName(), clazz);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+        IOHelper.close(classLoader);
+    }
+}
diff --git 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyCompilerRouteTest.java
 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyCompilerRouteTest.java
new file mode 100644
index 00000000000..7dcdb079522
--- /dev/null
+++ 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyCompilerRouteTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.processor.groovy;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.language.groovy.GroovyScriptCompiler;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+public class GroovyCompilerRouteTest extends CamelTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+
+        GroovyScriptCompiler compiler = new GroovyScriptCompiler();
+        compiler.setCamelContext(context);
+        compiler.setFolder("src/test/resources/myscript");
+        context.addService(compiler);
+
+        return context;
+    }
+
+    @Test
+    public void testCompilerRoute() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("I want to order 
2 gauda");
+
+        template.sendBodyAndHeader("direct:start", "Hello World", "amount", 2);
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:start")
+                        .setBody().groovy("""
+                                Dude d = new Dude()
+                                return d.order(header.amount)
+                                """)
+                        .to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyCompilerTest.java
 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyCompilerTest.java
new file mode 100644
index 00000000000..d3f74cdc11b
--- /dev/null
+++ 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyCompilerTest.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.processor.groovy;
+
+import java.lang.reflect.Method;
+
+import org.apache.camel.language.groovy.GroovyScriptCompiler;
+import org.apache.camel.support.ObjectHelper;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class GroovyCompilerTest extends CamelTestSupport {
+
+    @Test
+    public void testCompiler() throws Exception {
+        GroovyScriptCompiler compiler = new GroovyScriptCompiler();
+        compiler.setCamelContext(context);
+        compiler.setFolder("src/test/resources/myscript");
+        compiler.start();
+
+        Class<?> clazz = compiler.loadClass("Dude");
+        Object dude = ObjectHelper.newInstance(clazz);
+        Method m = clazz.getMethod("order", int.class);
+        Object o = ObjectHelper.newInstance(clazz);
+        Object out = m.invoke(o, 5);
+
+        Assertions.assertEquals("I want to order 5 gauda", out);
+    }
+
+}
diff --git a/components/camel-groovy/src/test/resources/myscript/Cheese.groovy 
b/components/camel-groovy/src/test/resources/myscript/Cheese.groovy
new file mode 100644
index 00000000000..722b9a035b3
--- /dev/null
+++ b/components/camel-groovy/src/test/resources/myscript/Cheese.groovy
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+class Cheese {
+
+  def name() {
+    return 'gauda'
+  }
+
+}
+
diff --git a/components/camel-groovy/src/test/resources/myscript/Dude.groovy 
b/components/camel-groovy/src/test/resources/myscript/Dude.groovy
new file mode 100644
index 00000000000..0a87932b16d
--- /dev/null
+++ b/components/camel-groovy/src/test/resources/myscript/Dude.groovy
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+class Dude {
+
+  def order(int a) {
+    def greeter = new Cheese()
+    return 'I want to order ' + a + ' ' + greeter.name()
+  }
+
+}
+

Reply via email to