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