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 <claus.ib...@gmail.com>
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);
         }

Reply via email to