You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2021/05/10 06:34:45 UTC

[camel] 04/06: CAMEL-16596: ScriptingLanguage SPI.

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

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

commit f79c25816ad2ed9db21e031712056ff3c62d24de
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Mon May 10 08:24:47 2021 +0200

    CAMEL-16596: ScriptingLanguage SPI.
---
 .../apache/camel/language/joor/JoorLanguage.java   |  26 ++-
 .../org/apache/camel/language/joor/JoorMethod.java |   3 +
 .../camel/language/joor/JoorScriptingCompiler.java | 214 +++++++++++++++++++++
 ...tionException.java => JoorScriptingMethod.java} |  26 +--
 .../language/joor/JoorScriptingLanguageTest.java   |  47 +++++
 components/camel-kamelet/pom.xml                   |   5 +
 .../kamelet/KameletLocalBeanJoorExternalTest.java  |  76 ++++++++
 .../src/test/resources/mybar.joor}                 |  18 +-
 8 files changed, 379 insertions(+), 36 deletions(-)

diff --git a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorLanguage.java b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorLanguage.java
index 9d5b883..b3788d6 100644
--- a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorLanguage.java
+++ b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorLanguage.java
@@ -23,7 +23,9 @@ import java.util.TreeSet;
 
 import org.apache.camel.Expression;
 import org.apache.camel.Predicate;
+import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.StaticService;
+import org.apache.camel.spi.ScriptingLanguage;
 import org.apache.camel.spi.annotations.Language;
 import org.apache.camel.support.ExpressionToPredicateAdapter;
 import org.apache.camel.support.LanguageSupport;
@@ -34,12 +36,13 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Language("joor")
-public class JoorLanguage extends LanguageSupport implements StaticService {
+public class JoorLanguage extends LanguageSupport implements ScriptingLanguage, StaticService {
 
     private static final Logger LOG = LoggerFactory.getLogger(JoorLanguage.class);
 
     private static Boolean java8;
     private final JoorCompiler compiler = new JoorCompiler();
+    private final JoorScriptingCompiler scriptingCompiler = new JoorScriptingCompiler();
     private final Set<String> imports = new TreeSet<>();
     private final Map<String, String> aliases = new HashMap<>();
 
@@ -83,6 +86,27 @@ public class JoorLanguage extends LanguageSupport implements StaticService {
     }
 
     @Override
+    public <T> T evaluate(String script, Map<String, Object> bindings, Class<T> resultType) {
+        Object out;
+        JoorScriptingMethod target = scriptingCompiler.compile(getCamelContext(), script, bindings, resultType, singleQuotes);
+        try {
+            if (bindings != null && !bindings.isEmpty()) {
+                Object[] args = bindings.values().toArray(new Object[0]);
+                out = target.evaluate(args);
+            } else {
+                out = target.evaluate();
+            }
+        } catch (Exception e) {
+            throw RuntimeCamelException.wrapRuntimeException(e);
+        }
+        if (out != null && resultType != null) {
+            return getCamelContext().getTypeConverter().convertTo(resultType, out);
+        } else {
+            return (T) out;
+        }
+    }
+
+    @Override
     public Predicate createPredicate(String expression) {
         return ExpressionToPredicateAdapter.toPredicate(createExpression(expression));
     }
diff --git a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorMethod.java b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorMethod.java
index ade1503..a1a8c03 100644
--- a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorMethod.java
+++ b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorMethod.java
@@ -22,6 +22,9 @@ import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 
+/**
+ * Interface for the compiled joor method when evaluating as Camel {@link org.apache.camel.Expression}.
+ */
 public interface JoorMethod {
 
     Object evaluate(CamelContext context, Exchange exchange, Message message, Object body, Optional<?> optionalBody)
diff --git a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingCompiler.java b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingCompiler.java
new file mode 100644
index 0000000..6527dd7
--- /dev/null
+++ b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingCompiler.java
@@ -0,0 +1,214 @@
+/*
+ * 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.joor;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.StaticService;
+import org.apache.camel.support.CamelContextHelper;
+import org.apache.camel.support.ScriptHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.StopWatch;
+import org.joor.Reflect;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JoorScriptingCompiler extends ServiceSupport implements StaticService {
+
+    private static final Pattern BEAN_INJECTION_PATTERN = Pattern.compile("(#bean:)([A-Za-z0-9-_]*)");
+
+    private static final Logger LOG = LoggerFactory.getLogger(JoorCompiler.class);
+    private static final AtomicInteger UUID = new AtomicInteger();
+    private Set<String> imports = new TreeSet<>();
+    private Map<String, String> aliases;
+    private int counter;
+    private long taken;
+
+    public Set<String> getImports() {
+        return imports;
+    }
+
+    public void setImports(Set<String> imports) {
+        this.imports = imports;
+    }
+
+    public Map<String, String> getAliases() {
+        return aliases;
+    }
+
+    public void setAliases(Map<String, String> aliases) {
+        this.aliases = aliases;
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+        if (counter > 0) {
+            LOG.info("jOOR scripting language compiled {} scripts in {} millis", counter, taken);
+        }
+    }
+
+    public JoorScriptingMethod compile(
+            CamelContext camelContext, String script, Map<String, Object> bindings, Class<?> resultType, boolean singleQuotes) {
+        StopWatch watch = new StopWatch();
+
+        JoorScriptingMethod answer;
+        String className = nextFQN();
+        String code = evalCode(camelContext, className, script, bindings, resultType, singleQuotes);
+        try {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Compiling code:\n\n{}\n", code);
+            }
+            Reflect ref = Reflect.compile(className, code);
+            Class<?> clazz = ref.type();
+            answer = (JoorScriptingMethod) clazz.getConstructor(CamelContext.class).newInstance(camelContext);
+        } catch (Exception e) {
+            throw new JoorCompilationException(className, code, e);
+        }
+
+        counter++;
+        taken += watch.taken();
+        return answer;
+    }
+
+    @SuppressWarnings("unchecked")
+    public String evalCode(
+            CamelContext camelContext, String fqn, String script, Map<String, Object> bindings, Class<?> resultType,
+            boolean singleQuotes) {
+        String qn = fqn.substring(0, fqn.lastIndexOf('.'));
+        String name = fqn.substring(fqn.lastIndexOf('.') + 1);
+
+        // reload script
+        script = ScriptHelper.resolveOptionalExternalScript(camelContext, script);
+
+        // trim text
+        script = script.trim();
+
+        Set<String> scriptImports = new LinkedHashSet<>();
+        Map<String, Class> scriptBeans = new HashMap<>();
+        script = evalDependencyInjection(camelContext, scriptImports, scriptBeans, script);
+
+        //  wrap text into a class method we can call
+        StringBuilder sb = new StringBuilder();
+        sb.append("package ").append(qn).append(";\n");
+        sb.append("\n");
+        sb.append("import java.util.*;\n");
+        sb.append("import java.util.concurrent.*;\n");
+        sb.append("import java.util.stream.*;\n");
+        sb.append("\n");
+        sb.append("import org.apache.camel.*;\n");
+        sb.append("import org.apache.camel.util.*;\n");
+        sb.append("import static org.apache.camel.language.joor.JoorHelper.*;\n");
+        sb.append("\n");
+        // custom imports
+        for (String i : imports) {
+            sb.append(i);
+            if (!i.endsWith(";")) {
+                sb.append(";");
+            }
+            sb.append("\n");
+        }
+        for (String i : scriptImports) {
+            sb.append("import ");
+            sb.append(i);
+            sb.append(";\n");
+        }
+        sb.append("\n");
+        sb.append("public class ").append(name).append(" implements org.apache.camel.language.joor.JoorScriptingMethod {\n");
+        sb.append("\n");
+        // local beans variables
+        for (Map.Entry<String, Class> entry : scriptBeans.entrySet()) {
+            sb.append("    private ").append(entry.getValue().getSimpleName()).append(" ").append(entry.getKey()).append(";\n");
+        }
+        sb.append("\n");
+
+        // constructor to lookup beans
+        sb.append("    public ").append(name).append("(CamelContext context) throws Exception {\n");
+        for (Map.Entry<String, Class> entry : scriptBeans.entrySet()) {
+            sb.append("        ").append(entry.getKey()).append(" = ").append("context.getRegistry().lookupByNameAndType(\"")
+                    .append(entry.getKey()).append("\", ").append(entry.getValue().getSimpleName()).append(".class);\n");
+        }
+        sb.append("    }\n");
+        sb.append("\n");
+
+        sb.append("    @Override\n");
+        sb.append(
+                "    public Object evaluate(Object... args) throws Exception {\n");
+        sb.append("        ");
+
+        // bindings as local variables
+        if (bindings != null) {
+            int i = 0;
+            for (Map.Entry<String, Object> bind : bindings.entrySet()) {
+                String vn = bind.getKey();
+                String cn = ObjectHelper.className(bind.getValue());
+                String b = String.format("        var %s = (%s) args[%s];\n", vn, cn, i++);
+                sb.append(b);
+            }
+            sb.append("\n");
+        }
+        if (!script.contains("return ")) {
+            sb.append("return ");
+        }
+
+        if (singleQuotes) {
+            // single quotes instead of double quotes, as its very annoying for string in strings
+            String quoted = script.replace('\'', '"');
+            sb.append(quoted);
+        } else {
+            sb.append(script);
+        }
+        if (!script.endsWith("}") && !script.endsWith(";")) {
+            sb.append(";");
+        }
+        sb.append("\n");
+        sb.append("    }\n");
+        sb.append("}\n");
+        sb.append("\n");
+
+        return sb.toString();
+    }
+
+    private String evalDependencyInjection(
+            CamelContext camelContext, Set<String> scriptImports, Map<String, Class> scriptBeans, String script) {
+        Matcher matcher = BEAN_INJECTION_PATTERN.matcher(script);
+        while (matcher.find()) {
+            String id = matcher.group(2);
+            Object bean = CamelContextHelper.mandatoryLookup(camelContext, id);
+            Class<?> type = bean.getClass();
+            scriptImports.add(type.getName());
+            scriptBeans.put(id, type);
+            script = matcher.replaceFirst(id);
+            matcher = BEAN_INJECTION_PATTERN.matcher(script);
+        }
+        return script;
+    }
+
+    private static String nextFQN() {
+        return "org.apache.camel.language.joor.compiled.scripting.JoorScripting" + UUID.incrementAndGet();
+    }
+
+}
diff --git a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorExpressionEvaluationException.java b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingMethod.java
similarity index 52%
rename from components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorExpressionEvaluationException.java
rename to components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingMethod.java
index 806e550..2d2eec6 100644
--- a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorExpressionEvaluationException.java
+++ b/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorScriptingMethod.java
@@ -16,27 +16,11 @@
  */
 package org.apache.camel.language.joor;
 
-import org.apache.camel.Exchange;
-import org.apache.camel.Expression;
-import org.apache.camel.ExpressionEvaluationException;
-
-public class JoorExpressionEvaluationException extends ExpressionEvaluationException {
-
-    private final String className;
-    private final String code;
-
-    public JoorExpressionEvaluationException(Expression expression, String className, String code, Exchange exchange,
-                                             Throwable cause) {
-        super(expression, "jOOR evaluation error for class: " + className + " with code:\n" + code, exchange, cause);
-        this.className = className;
-        this.code = code;
-    }
+/**
+ * Interface for the compiled joor scripting method when evaluating as scripting language.
+ */
+public interface JoorScriptingMethod {
 
-    public String getClassName() {
-        return className;
-    }
+    Object evaluate(Object... args) throws Exception;
 
-    public String getCode() {
-        return code;
-    }
 }
diff --git a/components/camel-joor/src/test/java/org/apache/camel/language/joor/JoorScriptingLanguageTest.java b/components/camel-joor/src/test/java/org/apache/camel/language/joor/JoorScriptingLanguageTest.java
new file mode 100644
index 0000000..97f00f0
--- /dev/null
+++ b/components/camel-joor/src/test/java/org/apache/camel/language/joor/JoorScriptingLanguageTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.joor;
+
+import org.apache.camel.spi.Language;
+import org.apache.camel.spi.ScriptingLanguage;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class JoorScriptingLanguageTest extends CamelTestSupport {
+
+    @Test
+    public void testScripting() throws Exception {
+        Language lan = context.resolveLanguage("joor");
+        Assertions.assertTrue(lan instanceof ScriptingLanguage);
+
+        ScriptingLanguage slan = (ScriptingLanguage) lan;
+        int num = slan.evaluate("2 * 3", null, int.class);
+        Assertions.assertEquals(6, num);
+
+        MyUser user = new MyUser();
+        user.setAge(33);
+        user.setName("Scott");
+        Map<String, Object> bindings = new LinkedHashMap<>();
+        bindings.put("user", user);
+        String out = slan.evaluate("'Hello ' + user.getName() + ' you are ' + user.getAge() + ' years old'", bindings, String.class);
+        Assertions.assertEquals("Hello Scott you are 33 years old", out);
+    }
+}
diff --git a/components/camel-kamelet/pom.xml b/components/camel-kamelet/pom.xml
index c443946..8cd7354 100644
--- a/components/camel-kamelet/pom.xml
+++ b/components/camel-kamelet/pom.xml
@@ -51,6 +51,11 @@
         </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
+            <artifactId>camel-joor</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
             <artifactId>camel-timer</artifactId>
             <scope>test</scope>
         </dependency>
diff --git a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanJoorExternalTest.java b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanJoorExternalTest.java
new file mode 100644
index 0000000..349587d
--- /dev/null
+++ b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanJoorExternalTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.component.kamelet;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.http.annotation.Obsolete;
+import org.junit.jupiter.api.Test;
+
+public class KameletLocalBeanJoorExternalTest extends CamelTestSupport {
+
+    @Test
+    public void testOne() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("Hi John we are going to Moes");
+
+        template.sendBody("direct:moe", "John");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    public void testTwo() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("Hi Jack we are going to Shamrock",
+                "Hi Mary we are going to Moes");
+
+        template.sendBody("direct:shamrock", "Jack");
+        template.sendBody("direct:moe", "Mary");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    // **********************************************
+    //
+    // test set-up
+    //
+    // **********************************************
+
+    @Obsolete
+    protected RoutesBuilder createRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                routeTemplate("whereTo")
+                        .templateParameter("bar") // name of bar
+                        .templateBean("myBar", "joor", "resource:classpath:mybar.joor")
+                        .from("kamelet:source")
+                        // must use {{myBar}} to refer to the local bean
+                        .to("bean:{{myBar}}");
+
+                from("direct:shamrock")
+                        .kamelet("whereTo?bar=Shamrock")
+                        .to("mock:result");
+
+                from("direct:moe")
+                        .kamelet("whereTo?bar=Moes")
+                        .to("mock:result");
+            }
+        };
+    }
+
+}
diff --git a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorMethod.java b/components/camel-kamelet/src/test/resources/mybar.joor
similarity index 69%
copy from components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorMethod.java
copy to components/camel-kamelet/src/test/resources/mybar.joor
index ade1503..f9c0bc9 100644
--- a/components/camel-joor/src/main/java/org/apache/camel/language/joor/JoorMethod.java
+++ b/components/camel-kamelet/src/test/resources/mybar.joor
@@ -14,17 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.language.joor;
-
-import java.util.Optional;
-
-import org.apache.camel.CamelContext;
-import org.apache.camel.Exchange;
-import org.apache.camel.Message;
-
-public interface JoorMethod {
-
-    Object evaluate(CamelContext context, Exchange exchange, Message message, Object body, Optional<?> optionalBody)
-            throws Exception;
-
-}
+var bean = new org.apache.camel.component.kamelet.MyInjectBar();
+// context is RouteTemplateContext
+bean.setBar(context.getProperty('bar', String.class));
+return bean;