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 295291820e9 CAMEL-21535: camel-groovy - Make it easier to validate
groovy script for low-code tools (#16545)
295291820e9 is described below
commit 295291820e9005f63a0fdeed70beba7a02f0ea67
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Dec 12 15:23:09 2024 +0100
CAMEL-21535: camel-groovy - Make it easier to validate groovy script for
low-code tools (#16545)
---
catalog/camel-catalog/pom.xml | 6 ++
.../org/apache/camel/catalog/CamelCatalogTest.java | 17 ++++
.../camel/language/groovy/GroovyExpression.java | 4 +-
.../camel/language/groovy/GroovyLanguage.java | 23 +++++
.../language/groovy/GroovyValidationException.java | 63 ++++++++++++
.../camel/language/groovy/GroovyLanguageTest.java | 27 ++++++
.../camel/catalog/impl/AbstractCamelCatalog.java | 106 ++++++++++++++++++++-
7 files changed, 243 insertions(+), 3 deletions(-)
diff --git a/catalog/camel-catalog/pom.xml b/catalog/camel-catalog/pom.xml
index 5a130061c7d..1c4706eb213 100644
--- a/catalog/camel-catalog/pom.xml
+++ b/catalog/camel-catalog/pom.xml
@@ -104,6 +104,12 @@
<artifactId>camel-jsonpath</artifactId>
<scope>test</scope>
</dependency>
+ <!-- for testing groovy language -->
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-groovy</artifactId>
+ <scope>test</scope>
+ </dependency>
<!-- for testing activemq component -->
<dependency>
diff --git
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
index 0863b9b0848..2b558750590 100644
---
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
+++
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
@@ -1126,6 +1126,23 @@ public class CamelCatalogTest {
assertEquals("$.store.book[?(@.price < 10)]", result.getText());
}
+ @Test
+ public void testValidateGroovyLanguage() {
+ LanguageValidationResult result =
catalog.validateLanguageExpression(null, "groovy", "4 * 3");
+ assertTrue(result.isSuccess());
+ assertEquals("4 * 3", result.getText());
+
+ var code = """
+ var a = 123;
+ println a */ 2;
+ """;
+ result = catalog.validateLanguageExpression(null, "groovy", code);
+ assertFalse(result.isSuccess());
+ assertEquals(code, result.getText());
+ assertEquals(23, result.getIndex());
+ assertEquals("Unexpected input: '*' @ line 2, column 11.",
result.getShortError());
+ }
+
@Test
public void testSpringCamelContext() {
String xml = catalog.springSchemaAsXml();
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 b5adb8215a6..ebbe38c75d9 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
@@ -62,7 +62,7 @@ public class GroovyExpression extends ExpressionSupport {
}
@SuppressWarnings("unchecked")
- private Script instantiateScript(Exchange exchange, Map<String, Object>
globalVariables) {
+ protected Script instantiateScript(Exchange exchange, Map<String, Object>
globalVariables) {
// Get the script from the cache, or create a new instance
GroovyLanguage language = (GroovyLanguage)
exchange.getContext().resolveLanguage("groovy");
Set<GroovyShellFactory> shellFactories =
exchange.getContext().getRegistry().findByType(GroovyShellFactory.class);
@@ -87,7 +87,7 @@ public class GroovyExpression extends ExpressionSupport {
return ObjectHelper.newInstance(scriptClass, Script.class);
}
- private Binding createBinding(Exchange exchange, Map<String, Object>
globalVariables) {
+ protected Binding createBinding(Exchange exchange, Map<String, Object>
globalVariables) {
Map<String, Object> map = new HashMap<>(globalVariables);
ExchangeHelper.populateVariableMap(exchange, map, true);
map.put("log", LOG);
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 3a170b24f8e..a03145305ff 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
@@ -23,11 +23,13 @@ import java.util.Map;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
+import org.apache.camel.Exchange;
import org.apache.camel.Service;
import org.apache.camel.spi.CamelEvent;
import org.apache.camel.spi.EventNotifier;
import org.apache.camel.spi.ScriptingLanguage;
import org.apache.camel.spi.annotations.Language;
+import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.LRUCacheFactory;
import org.apache.camel.support.ObjectHelper;
import org.apache.camel.support.SimpleEventNotifierSupport;
@@ -156,6 +158,27 @@ public class GroovyLanguage extends TypedLanguageSupport
implements ScriptingLan
return getCamelContext().getTypeConverter().convertTo(resultType,
value);
}
+ // use by tooling
+ public boolean validateExpression(String expression) throws
GroovyValidationException {
+ final Exchange dummy = new DefaultExchange(getCamelContext());
+ Map<String, Object> globalVariables = new HashMap<>();
+
+ try {
+ GroovyExpression ge = createExpression(expression);
+ Script script = ge.instantiateScript(dummy, globalVariables);
+ script.setBinding(ge.createBinding(dummy, globalVariables));
+ script.run();
+ } catch (Exception e) {
+ throw new GroovyValidationException(expression, e);
+ }
+ return true;
+ }
+
+ // use by tooling
+ public boolean validatePredicate(String expression) throws
GroovyValidationException {
+ return validateExpression(expression);
+ }
+
Class<Script> getScriptFromCache(String script) {
final GroovyClassService cached = scriptCache.get(script);
if (cached == null) {
diff --git
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyValidationException.java
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyValidationException.java
new file mode 100644
index 00000000000..c8f0d883f8f
--- /dev/null
+++
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyValidationException.java
@@ -0,0 +1,63 @@
+/*
+ * 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.LineNumberReader;
+import java.io.StringReader;
+
+import org.codehaus.groovy.control.MultipleCompilationErrorsException;
+import org.codehaus.groovy.control.messages.Message;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+
+/**
+ * Exception when validating groovy scripts.
+ */
+public class GroovyValidationException extends Exception {
+
+ private final String script;
+
+ public GroovyValidationException(String script, Throwable cause) {
+ super(cause);
+ this.script = script;
+ }
+
+ public String getScript() {
+ return script;
+ }
+
+ public int getIndex() {
+ if (getCause() instanceof MultipleCompilationErrorsException me) {
+ Message gm = me.getErrorCollector().getLastError();
+ if (gm instanceof SyntaxErrorMessage sem) {
+ LineNumberReader lr = new LineNumberReader(new
StringReader(script));
+ int pos = -1;
+ for (int i = 1; i < sem.getCause().getLine(); i++) {
+ try {
+ String line = lr.readLine();
+ pos += line.length() + System.lineSeparator().length();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ pos += sem.getCause().getStartColumn();
+ return pos;
+ }
+ }
+ return -1;
+ }
+
+}
diff --git
a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyLanguageTest.java
b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyLanguageTest.java
index fb0540abe2d..9758b71d147 100644
---
a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyLanguageTest.java
+++
b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyLanguageTest.java
@@ -17,8 +17,14 @@
package org.apache.camel.language.groovy;
import org.apache.camel.test.junit5.LanguageTestSupport;
+import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
public class GroovyLanguageTest extends LanguageTestSupport {
@Test
@@ -43,6 +49,27 @@ public class GroovyLanguageTest extends LanguageTestSupport {
assertExpression("exchangeProperty.myProp2", 123);
}
+ @Test
+ public void testValidateExpression() throws Exception {
+ GroovyLanguage g = new GroovyLanguage();
+ g.setCamelContext(context);
+
+ assertTrue(g.validateExpression("2 * 3"));
+ assertTrue(g.validateExpression("exchange.getExchangeId()"));
+ assertTrue(g.validatePredicate("2 * 3 > 4"));
+
+ try {
+ g.validateExpression("""
+ var a = 123;
+ println a */ 2;
+ """);
+ fail("Should throw error");
+ } catch (GroovyValidationException e) {
+ assertEquals(23, e.getIndex());
+ assertInstanceOf(MultipleCompilationErrorsException.class,
e.getCause());
+ }
+ }
+
@Override
protected String getLanguageName() {
return "groovy";
diff --git
a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
index f1d677d596c..238dde62451 100644
---
a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
+++
b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.catalog.impl;
+import java.io.LineNumberReader;
+import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
@@ -1322,7 +1324,7 @@ public abstract class AbstractCamelCatalog {
// if there are {{ }}} property placeholders then we need to resolve
them to something else
// as the simple parse cannot resolve them before parsing as we dont
run the actual Camel application
// with property placeholders setup so we need to dummy this by
replace the {{ }} to something else
- // therefore we use an more unlikely character: {{XXX}} to ~^XXX^~
+ // therefore we use a more unlikely character: {{XXX}} to ~^XXX^~
String resolved = simple.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~");
LanguageValidationResult answer = new LanguageValidationResult(simple);
@@ -1412,9 +1414,109 @@ public abstract class AbstractCamelCatalog {
return answer;
}
+ private LanguageValidationResult doValidateGroovy(ClassLoader classLoader,
String groovy, boolean predicate) {
+ if (classLoader == null) {
+ classLoader = getClass().getClassLoader();
+ }
+
+ // if there are {{ }}} property placeholders then we need to resolve
them to something else
+ // as the simple parse cannot resolve them before parsing as we dont
run the actual Camel application
+ // with property placeholders setup so we need to dummy this by
replace the {{ }} to something else
+ // therefore we use a more unlikely character: {{XXX}} to ~^XXX^~
+ String resolved = groovy.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~");
+
+ LanguageValidationResult answer = new LanguageValidationResult(groovy);
+
+ Object context;
+ Object instance = null;
+ Class<?> clazz;
+
+ try {
+ // need a simple camel context for the groovy language parser to
be able to parse
+ clazz =
classLoader.loadClass("org.apache.camel.impl.engine.SimpleCamelContext");
+ context =
clazz.getDeclaredConstructor(boolean.class).newInstance(false);
+ clazz =
classLoader.loadClass("org.apache.camel.language.groovy.GroovyLanguage");
+ instance = clazz.getDeclaredConstructor().newInstance();
+ clazz = classLoader.loadClass("org.apache.camel.CamelContext");
+ instance.getClass().getMethod("setCamelContext",
clazz).invoke(instance, context);
+ } catch (Exception e) {
+ clazz = null;
+ answer.setError(e.getMessage());
+ }
+
+ if (clazz != null) {
+ Throwable cause = null;
+ try {
+ if (predicate) {
+ instance.getClass().getMethod("validatePredicate",
String.class).invoke(instance, resolved);
+ } else {
+ instance.getClass().getMethod("validateExpression",
String.class).invoke(instance, resolved);
+ }
+ } catch (InvocationTargetException e) {
+ cause = e.getTargetException();
+ } catch (Exception e) {
+ cause = e;
+ }
+
+ if (cause != null) {
+
+ // reverse ~^XXX^~ back to {{XXX}}
+ String errMsg = cause.getMessage();
+ errMsg = errMsg.replaceAll("\\~\\^(.+)\\^\\~", "{{$1}}");
+
+ answer.setError(errMsg);
+
+ // is it simple parser exception then we can grab the index
where the problem is
+ if
(cause.getClass().getName().equals("org.apache.camel.language.groovy.GroovyValidationException"))
{
+ try {
+ // we need to grab the index field from those simple
parser exceptions
+ Method method = cause.getClass().getMethod("getIndex");
+ Object result = method.invoke(cause);
+ if (result != null) {
+ int index = (int) result;
+ answer.setIndex(index);
+ }
+ } catch (Exception i) {
+ // ignore
+ }
+ }
+
+ // we need to grab the short message field from this simple
syntax exception
+ if (answer.getShortError() == null) {
+ // fallback and try to make existing message short instead
+ String msg = answer.getError();
+ // grab everything before " @ " which would be regarded as
the short message
+ LineNumberReader lnr = new LineNumberReader(new
StringReader(msg));
+ try {
+ String line = lnr.readLine();
+ do {
+ if (line.contains(" @ ")) {
+ // skip leading Scrip_xxxx.groovy: N:
+ if (line.startsWith("Script_") &&
StringHelper.countChar(line, ':') > 2) {
+ line = StringHelper.after(line, ":", line);
+ line = StringHelper.after(line, ":", line);
+ line = line.trim();
+ }
+ answer.setShortError(line);
+ break;
+ }
+ line = lnr.readLine();
+ } while (line != null);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ return answer;
+ }
+
public LanguageValidationResult validateLanguagePredicate(ClassLoader
classLoader, String language, String text) {
if ("simple".equals(language)) {
return doValidateSimple(classLoader, text, true);
+ } else if ("groovy".equals(language)) {
+ return doValidateGroovy(classLoader, text, true);
} else {
return doValidateLanguage(classLoader, language, text, true);
}
@@ -1423,6 +1525,8 @@ public abstract class AbstractCamelCatalog {
public LanguageValidationResult validateLanguageExpression(ClassLoader
classLoader, String language, String text) {
if ("simple".equals(language)) {
return doValidateSimple(classLoader, text, false);
+ } else if ("groovy".equals(language)) {
+ return doValidateGroovy(classLoader, text, false);
} else {
return doValidateLanguage(classLoader, language, text, false);
}