Skip to content

Commit

Permalink
feat(JS): evaluate JS on GraalVM
Browse files Browse the repository at this point in the history
  • Loading branch information
saig0 authored and jonathanlukas committed Oct 24, 2023
1 parent 5086aba commit 4b3c97d
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 11 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

[![Compatible with: Camunda Platform 8](https://img.shields.io/badge/Compatible%20with-Camunda%20Platform%208-0072Ce)](https://github.com/camunda-community-hub/community/blob/main/extension-lifecycle.md#compatiblilty)

Available script languages:
* [javascript](https://www.graalvm.org/) (GraalVM JS)
* [groovy](http://groovy-lang.org/)
* [feel](https://github.com/camunda/feel-scala)

_This is a community project meant for playing around with Zeebe. It is not officially supported by the Zeebe Team (i.e. no gurantees). Everybody is invited to contribute!_
A Zeebe worker to evaluate scripts (i.e. script tasks). Scripts are useful for prototyping, to do (simple) calculations, or creating/modifying variables.

## Usage
Expand Down
23 changes: 19 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<name>Zeebe Script Worker</name>
Expand Down Expand Up @@ -29,6 +31,7 @@

<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<version.graalvm>1.0.0-rc9</version.graalvm>

<!-- release parent settings -->
<nexus.snapshot.repository>https://artifacts.camunda.com/artifactory/zeebe-io-snapshots/
Expand Down Expand Up @@ -114,6 +117,18 @@
<version>${version.feel}</version>
</dependency>

<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>${version.graalvm}</version>
</dependency>

<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>${version.graalvm}</version>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
Expand Down Expand Up @@ -241,9 +256,9 @@
</gpgArguments>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</plugins>
</build>
</profile>
</profiles>

<repositories>
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/io/zeebe/script/GraalEvaluator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright © 2017 camunda services GmbH ([email protected])
*
* Licensed 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 io.zeebe.script;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Value;

public class GraalEvaluator {

private static final String LANGUAGE_ID_JS = "js";

public static final List<String> SUPPORTED_LANGUAGES = Arrays.asList("js", "javascript");

public Object evaluate(String language, String script, Map<String, Object> variables)
throws PolyglotException, IllegalArgumentException, IllegalStateException {

// this throws a FileSystemNotFoundException
final Context context = Context.create(LANGUAGE_ID_JS);

final Value bindings = context.getBindings(LANGUAGE_ID_JS);
variables.forEach((key, value) -> bindings.putMember(key, value));

final Value result = context.eval(LANGUAGE_ID_JS, script);

return mapValueToObject(result);
}

private Object mapValueToObject(final Value value) {
if (value.isNull()) {
return null;

} else if (value.isBoolean()) {
return value.asBoolean();

} else if (value.isString()) {
return value.asString();

} else if (value.isNumber()) {
if (value.fitsInInt()) {
return value.asInt();
} else if (value.fitsInLong()) {
return value.asLong();
} else if (value.fitsInFloat()) {
return value.asFloat();
} else {
return value.asDouble();
}

} else if (value.hasArrayElements()) {
return LongStream.range(0, value.getArraySize())
.mapToObj(i -> mapValueToObject(value.getArrayElement(i)))
.collect(Collectors.toList());

} else if (value.hasMembers()) {
return value
.getMemberKeys()
.stream()
.collect(
Collectors.toMap(Function.identity(), key -> mapValueToObject(value.getMember(key))));
}

return "unknown: " + value.toString();
}
}
49 changes: 49 additions & 0 deletions src/main/java/io/zeebe/script/ScriptEngineEvaluator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright © 2017 camunda services GmbH ([email protected])
*
* Licensed 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 io.zeebe.script;

import java.util.HashMap;
import java.util.Map;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptEngineEvaluator {

private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();

private final Map<String, ScriptEngine> cachedScriptEngines = new HashMap<>();

public Object evaluate(String language, String script, Map<String, Object> variables)
throws ScriptException {

final ScriptEngine scriptEngine =
cachedScriptEngines.computeIfAbsent(language, scriptEngineManager::getEngineByName);

if (scriptEngine == null) {
final String msg = String.format("No script engine found with name '%s'", language);
throw new RuntimeException(msg);
}

final ScriptContext context = scriptEngine.getContext();
final Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.putAll(variables);

return scriptEngine.eval(script, context);
}
}
14 changes: 13 additions & 1 deletion src/main/java/io/zeebe/script/ScriptEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
import javax.script.ScriptException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map;
import org.springframework.stereotype.Component;

@Component
public class ScriptEvaluator {

private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
Expand All @@ -31,6 +34,8 @@ public class ScriptEvaluator {
Map.of("mustache", new MustacheEvaluator());

private final Map<String, ScriptEngine> cachedScriptEngines = new HashMap<>();
private final GraalEvaluator graalEvaluator = new GraalEvaluator();
private final ScriptEngineEvaluator scriptEngineEvaluator = new ScriptEngineEvaluator();

public Object evaluate(String language, String script, Map<String, Object> variables) {

Expand All @@ -55,7 +60,14 @@ private Object evalWithScriptEngine(
try {
return eval(scriptEngine, script, variables);

} catch (ScriptException e) {
if (GraalEvaluator.SUPPORTED_LANGUAGES.contains(language)) {
return graalEvaluator.evaluate(language, script, variables);

} else {
return scriptEngineEvaluator.evaluate(language, script, variables);
}

} catch (Exception e) {
final String msg = String.format("Failed to evaluate script '%s' (%s)", script, language);
throw new RuntimeException(msg, e);
}
Expand Down
19 changes: 14 additions & 5 deletions src/test/java/io/zeebe/script/EvaluationJavaScriptTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
import static org.assertj.core.api.Assertions.entry;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

public class EvaluationJavaScriptTest {
Expand All @@ -33,7 +33,7 @@ public void shouldReturnNumber() {
final Object result =
scriptEvaluator.evaluate("javascript", "x * 2", Collections.singletonMap("x", 2));

assertThat(result).isEqualTo(4.0);
assertThat(result).isEqualTo(4);
}

@Test
Expand Down Expand Up @@ -67,10 +67,19 @@ public void shouldReturnInlineObject() {
final Map<String, Object> result =
(Map<String, Object>)
scriptEvaluator.evaluate(
"javascript",
"result = {'foo':foo,'bar':'bar'}",
Collections.singletonMap("foo", 123));
"javascript", "({'foo':foo,'bar':'bar'})", Collections.singletonMap("foo", 123));

assertThat(result).hasSize(2).contains(entry("bar", "bar"), entry("foo", 123));
}

@Test
public void shouldReturnArray() {

@SuppressWarnings("unchecked")
final List<String> result =
(List<String>)
scriptEvaluator.evaluate("javascript", "['foo','bar']", Collections.emptyMap());

assertThat(result).hasSize(2).contains("foo", "bar");
}
}
2 changes: 1 addition & 1 deletion src/test/java/io/zeebe/script/ScriptEvaluatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void shouldEvaluateKotlinWithVariables() {
@Test
public void shouldThrowExceptionIfScriptEngineNotFound() {
assertThatThrownBy(() -> scriptEvaluator.evaluate("foobar", "", Collections.emptyMap()))
.hasMessage("No script engine found with name 'foobar'");
.hasCause(new RuntimeException("No script engine found with name 'foobar'"));
}

@Test
Expand Down

0 comments on commit 4b3c97d

Please sign in to comment.