diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2fe8b27..ea27500 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,7 +56,7 @@ jobs: with: upload_url: ${{ github.event.release.upload_url }} asset_path: ${{ steps.release.outputs.artifacts_archive_path }} - asset_name: camunda-7-to-8-migration.zip + asset_name: script-connector.zip asset_content_type: application/zip - name: Publish Unit Test Results id: publish diff --git a/.gitignore b/.gitignore index 4213124..9b09632 100644 --- a/.gitignore +++ b/.gitignore @@ -21,11 +21,11 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -/.classpath -/.project -/.settings -/target - -# intellij +target .idea *.iml + +#eclipse +**.project +**.classpath +**.settings diff --git a/README.md b/README.md index 197890b..c51e9b0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# zeebe-script-worker +# Skript Connector [![](https://img.shields.io/badge/Community%20Extension-An%20open%20source%20community%20maintained%20project-FF4700)](https://github.com/camunda-community-hub/community) [![](https://img.shields.io/badge/Lifecycle-Stable-brightgreen)](https://github.com/Camunda-Community-Hub/community/blob/main/extension-lifecycle.md#stable-) @@ -6,12 +6,17 @@ [![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) - -_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. +_This is a community project that provides a connector. It is not officially supported by Camunda. Everybody is invited to contribute!_ +A connector to evaluate scripts (i.e. script tasks) that are not written in FEEL. Scripts are useful for prototyping, to do (simple) calculations, or creating/modifying variables. ## Usage +### Legacy + +The legacy connector provides compatibility with the previous implementation `zeebe-script-worker`. + +>The context does not offer access to `job` or `zeebeClient` anymore. + Example BPMN with service task: ```xml @@ -21,6 +26,7 @@ Example BPMN with service task: + @@ -30,67 +36,67 @@ Example BPMN with service task: * required custom headers: * `language` - the name of the script language * `script` - the script to evaluate -* available context/variables in script: - * `job` (ActivatedJob) - the current job - * `zeebeClient` (ZeebeClient) - the client of the worker -* the result of the evaluation is passed as `result` variable + * `resultVariable` - the result of the evaluation is passed to this variable + +### Connector -Available script languages: +The connector provides an [element template](./connector/element-templates/script-connector.json) that can be used to configure it. + +### Script languages + +Available script languages are by default: * [javascript](https://www.graalvm.org/) (GraalVM JS) * [groovy](http://groovy-lang.org/) * [mustache](http://mustache.github.io/mustache.5.html) * [kotlin](https://kotlinlang.org/) +To register new script languages, you can use the `ScriptEngineFactory` to register any JSR-223 compliant script engine. + +If you want to provide a non-compliant implementation, you can use the [`ScriptEvaluatorExtension`](./connector/src/main/java/io/camunda/community/connector/script/spi/ScriptEvaluatorExtension.java) SPI. + +To register custom file extensions, you can use the [`LanguageProviderExtension`](./connector/src/main/java/io/camunda/community/connector/script/spi/LanguageProviderExtension.java) SPI. + ## Install ### Docker -The docker image for the worker is published on [GitHub Packages](https://github.com/orgs/camunda-community-hub/packages/container/package/zeebe-script-worker). +For a local setup, the repository contains a [docker-compose file](docker/docker-compose.yml). It starts a Zeebe broker and both (standalone and bundled) containers. ``` -docker pull ghcr.io/camunda-community-hub/zeebe-script-worker:1.2.0 +mvn clean package +cd docker +docker-compose up ``` -* configure the connection to the Zeebe broker by setting `zeebe.client.broker.contactPoint` (default: `localhost:26500`) -For a local setup, the repository contains a [docker-compose file](docker/docker-compose.yml). It starts a Zeebe broker and the worker. +#### Standalone Runtime + +The docker image for the connector runtime is published as GitHub package. ``` -cd docker -docker-compose up +docker pull ghcr.io/camunda-community-hub/script-connector/runtime:latest ``` -### Manual +Configure the connection to the Zeebe broker by setting the environment property `ZEEBE_CLIENT_BROKER_GATEWAY-ADDRESS` (default: `localhost:26500`) -1. Download the latest [worker JAR](https://github.com/zeebe-io/zeebe-script-worker/releases) _(zeebe-script-worker-%{VERSION}.jar -)_ +The docker-compose file shows an example how this works. -1. Start the worker - `java -jar zeebe-script-worker-{VERSION}.jar` +#### Bundled Runtime -### Configuration +To run the connector inside the bundle, you can use the shaded jar. -The worker is a Spring Boot application that uses the [Spring Zeebe Starter](https://github.com/zeebe-io/spring-zeebe). The configuration can be changed via environment variables or an `application.yaml` file. See also the following resources: -* [Spring Zeebe Configuration](https://github.com/zeebe-io/spring-zeebe#configuring-zeebe-connection) -* [Spring Boot Configuration](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config) +The docker-compose file shows an example how this works. -``` -zeebe: - client: - worker: - defaultName: script-worker - defaultType: script - threads: 3 - - job.timeout: 10000 - broker.contactPoint: 127.0.0.1:26500 - security.plaintext: true -``` +### Manual + +#### Standalone Runtime -## Build from Source +1. Download the runtime jar `script-connector-runtime-{VERSION}.jar` +2. Start the connector runtime `java -jar script-connector-runtime-{VERSION}.jar` -Build with Maven +#### Bundled Runtime -`mvn clean install` +1. Download the shaded connector jar `script-connector-{VERSION}-shaded.jar` +2. Copy it to your connector runtime. ## Code of Conduct diff --git a/connector/element-templates/script-connector.json b/connector/element-templates/script-connector.json new file mode 100644 index 0000000..099623b --- /dev/null +++ b/connector/element-templates/script-connector.json @@ -0,0 +1,163 @@ +{ + "$schema" : "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name" : "Script Connector", + "id" : "io.camunda.community:script-connector", + "description" : "A connector to execute a script", + "version" : 1, + "category" : { + "id" : "connectors", + "name" : "Connectors" + }, + "appliesTo" : [ "bpmn:Task" ], + "elementType" : { + "value" : "bpmn:ServiceTask" + }, + "groups" : [ { + "id" : "default", + "label" : "Properties" + }, { + "id" : "output", + "label" : "Output mapping" + }, { + "id" : "error", + "label" : "Error handling" + }, { + "id" : "retries", + "label" : "Retries" + } ], + "properties" : [ { + "value" : "io.camunda.community:script-connector", + "binding" : { + "type" : "zeebe:taskDefinition:type" + }, + "type" : "Hidden" + }, { + "id" : "script.type", + "label" : "Type", + "group" : "default", + "binding" : { + "name" : "script.type", + "type" : "zeebe:input" + }, + "type" : "Dropdown", + "choices" : [ { + "name" : "Embedded", + "value" : "embedded" + }, { + "name" : "Resource", + "value" : "resource" + } ] + }, { + "id" : "script.embedded", + "label" : "Script", + "description" : "The script to be executed", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "default", + "binding" : { + "name" : "script.embedded", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "script.type", + "equals" : "embedded" + }, + "type" : "String" + }, { + "id" : "script.language", + "label" : "Script Language", + "description" : "The language the script uses. By default, the ones available are: javascript, groovy, kotlin, mustache", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "default", + "binding" : { + "name" : "script.language", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "script.type", + "equals" : "embedded" + }, + "type" : "String" + }, { + "id" : "script.resource", + "label" : "Script resource", + "description" : "The resource that should be executed. Should be prefixed with 'classpath:' for a classpath resource, 'file:' for a file system resource. If none of these prefixes matches, it will attempt to load the provided resource as URL.", + "optional" : false, + "constraints" : { + "notEmpty" : true + }, + "feel" : "optional", + "group" : "default", + "binding" : { + "name" : "script.resource", + "type" : "zeebe:input" + }, + "condition" : { + "property" : "script.type", + "equals" : "resource" + }, + "type" : "String" + }, { + "id" : "context", + "label" : "Script context", + "description" : "The context that is available to the script", + "optional" : false, + "feel" : "required", + "group" : "default", + "binding" : { + "name" : "context", + "type" : "zeebe:input" + }, + "type" : "String" + }, { + "id" : "resultVariable", + "label" : "Result variable", + "description" : "Name of variable to store the response in", + "group" : "output", + "binding" : { + "key" : "resultVariable", + "type" : "zeebe:taskHeader" + }, + "type" : "String" + }, { + "id" : "resultExpression", + "label" : "Result expression", + "description" : "Expression to map the response into process variables", + "feel" : "required", + "group" : "output", + "binding" : { + "key" : "resultExpression", + "type" : "zeebe:taskHeader" + }, + "type" : "Text" + }, { + "id" : "errorExpression", + "label" : "Error expression", + "description" : "Expression to handle errors. Details in the documentation.", + "feel" : "required", + "group" : "error", + "binding" : { + "key" : "errorExpression", + "type" : "zeebe:taskHeader" + }, + "type" : "Text" + }, { + "id" : "retryBackoff", + "label" : "Retry backoff", + "description" : "ISO-8601 duration to wait between retries", + "value" : "PT0S", + "group" : "retries", + "binding" : { + "key" : "retryBackoff", + "type" : "zeebe:taskHeader" + }, + "type" : "Hidden" + } ] +} \ No newline at end of file diff --git a/connector/pom.xml b/connector/pom.xml new file mode 100644 index 0000000..0b6c1cc --- /dev/null +++ b/connector/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + Script Connector + script-connector + + + io.camunda.connectors.community + script-connector-parent + 1.2.1-SNAPSHOT + + + + + io.camunda.connector + connector-core + provided + + + io.camunda.connector + element-template-generator + true + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + + com.samskivert + jmustache + + + org.codehaus.groovy + groovy-all + + + org.graalvm.js + js + + + org.graalvm.js + js-scriptengine + + + org.jetbrains.kotlin + kotlin-scripting-jsr223 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + io.camunda.spring + spring-boot-starter-camunda-test + test + + + io.camunda.connector + spring-boot-starter-camunda-connectors + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + false + + + + + + + package + + shade + + + + + + io.camunda.connector + element-template-generator-maven-plugin + + + io.camunda.community.connector.script.ScriptConnector + + + + + + diff --git a/connector/src/main/java/io/camunda/community/connector/script/LanguageProvider.java b/connector/src/main/java/io/camunda/community/connector/script/LanguageProvider.java new file mode 100644 index 0000000..ae80a83 --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/LanguageProvider.java @@ -0,0 +1,30 @@ +package io.camunda.community.connector.script; + +import io.camunda.community.connector.script.spi.LanguageProviderExtension; +import java.util.Properties; + +public class LanguageProvider { + + private final Properties properties; + + public LanguageProvider() { + properties = new Properties(); + LanguageProviderExtension.load().forEach(e -> properties.putAll(e.getLanguages())); + } + + public LanguageProvider(Properties properties) { + this(); + this.properties.putAll(properties); + } + + public String getLanguageForScriptResource(String scriptResource) { + String fileExtension = scriptResource.substring(scriptResource.lastIndexOf(".") + 1); + String language = properties.getProperty(fileExtension); + if (language == null) { + throw new IllegalStateException( + String.format( + "Could not determine script language from file suffix '%s'", fileExtension)); + } + return language; + } +} diff --git a/connector/src/main/java/io/camunda/community/connector/script/ScriptConnector.java b/connector/src/main/java/io/camunda/community/connector/script/ScriptConnector.java new file mode 100644 index 0000000..cd10cfa --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/ScriptConnector.java @@ -0,0 +1,73 @@ +package io.camunda.community.connector.script; + +import io.camunda.community.connector.script.ScriptConnectorInput.Type; +import io.camunda.community.connector.script.ScriptConnectorInput.Type.Embedded; +import io.camunda.community.connector.script.ScriptConnectorInput.Type.Resource; +import io.camunda.connector.api.annotation.OutboundConnector; +import io.camunda.connector.api.outbound.OutboundConnectorContext; +import io.camunda.connector.api.outbound.OutboundConnectorFunction; +import io.camunda.connector.generator.annotation.ElementTemplate; + +@OutboundConnector( + type = ScriptConnector.SCRIPT_CONNECTOR_TYPE, + name = "script-connector", + inputVariables = {"script", "context"}) +@ElementTemplate( + id = ScriptConnector.SCRIPT_CONNECTOR_TYPE, + name = "Script Connector", + version = 1, + inputDataClass = ScriptConnectorInput.class, + description = "A connector to execute a script") +public class ScriptConnector implements OutboundConnectorFunction { + public static final String SCRIPT_CONNECTOR_TYPE = "io.camunda.community:script-connector"; + + private final ScriptEvaluator scriptEvaluator; + private final ScriptResourceProvider scriptResourceProvider; + private final LanguageProvider languageProvider; + + public ScriptConnector() { + scriptEvaluator = new ScriptEvaluator(); + scriptResourceProvider = new ScriptResourceProvider(); + languageProvider = new LanguageProvider(); + } + + public ScriptConnector( + ScriptEvaluator scriptEvaluator, + ScriptResourceProvider scriptResourceProvider, + LanguageProvider languageProvider) { + this.scriptEvaluator = scriptEvaluator; + this.scriptResourceProvider = scriptResourceProvider; + this.languageProvider = languageProvider; + } + + @Override + public Object execute(OutboundConnectorContext outboundConnectorContext) { + ScriptConnectorInput scriptConnectorInput = + outboundConnectorContext.bindVariables(ScriptConnectorInput.class); + String script = extractScript(scriptConnectorInput); + String language = extractLanguage(scriptConnectorInput); + return scriptEvaluator.evaluate(language, script, scriptConnectorInput.context()); + } + + private String extractLanguage(ScriptConnectorInput scriptConnectorInput) { + Type script = scriptConnectorInput.script(); + if (script instanceof Embedded e) { + return e.language(); + } else if (script instanceof Resource r) { + return languageProvider.getLanguageForScriptResource(r.resource()); + } else { + throw new IllegalStateException("No script or resource has been provided"); + } + } + + private String extractScript(ScriptConnectorInput scriptConnectorInput) { + Type script = scriptConnectorInput.script(); + if (script instanceof Embedded e) { + return e.embedded(); + } else if (script instanceof Resource r) { + return scriptResourceProvider.provideScript(r.resource()); + } else { + throw new IllegalStateException("No script or resource has been provided"); + } + } +} diff --git a/connector/src/main/java/io/camunda/community/connector/script/ScriptConnectorInput.java b/connector/src/main/java/io/camunda/community/connector/script/ScriptConnectorInput.java new file mode 100644 index 0000000..9b6dffd --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/ScriptConnectorInput.java @@ -0,0 +1,53 @@ +package io.camunda.community.connector.script; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import io.camunda.community.connector.script.ScriptConnectorInput.Type.Embedded; +import io.camunda.community.connector.script.ScriptConnectorInput.Type.Resource; +import io.camunda.connector.generator.annotation.TemplateProperty; +import io.camunda.connector.generator.dsl.Property.FeelMode; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record ScriptConnectorInput( + @TemplateProperty(label = "Script description", description = "How the script is implemented") + @NotNull + @Valid + Type script, + @TemplateProperty( + label = "Script context", + feel = FeelMode.required, + description = "The context that is available to the script") + Map context) { + + @JsonTypeInfo(use = Id.NAME, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Embedded.class, name = "embedded"), + @JsonSubTypes.Type(value = Resource.class, name = "resource") + }) + sealed interface Type { + record Embedded( + @TemplateProperty(label = "Script", description = "The script to be executed") @NotNull + String embedded, + @TemplateProperty( + label = "Script Language", + description = + "The language the script uses. By default, the ones available are: javascript, groovy, kotlin, mustache") + @NotNull + String language) + implements Type {} + + record Resource( + @TemplateProperty( + label = "Script resource", + description = + "The resource that should be executed. Should be prefixed with 'classpath:' for a classpath resource, 'file:' for a file system resource. If none of these prefixes matches, it will attempt to load the provided resource as URL.") + @NotNull + String resource) + implements Type {} + } +} diff --git a/connector/src/main/java/io/camunda/community/connector/script/ScriptConnectorLegacy.java b/connector/src/main/java/io/camunda/community/connector/script/ScriptConnectorLegacy.java new file mode 100644 index 0000000..cad0647 --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/ScriptConnectorLegacy.java @@ -0,0 +1,57 @@ +package io.camunda.community.connector.script; + +import static java.util.Optional.*; + +import io.camunda.connector.api.annotation.OutboundConnector; +import io.camunda.connector.api.outbound.OutboundConnectorContext; +import io.camunda.connector.api.outbound.OutboundConnectorFunction; +import java.util.Map; + +@OutboundConnector( + type = "script", + name = "script-connector-legacy", + inputVariables = {}) +public class ScriptConnectorLegacy implements OutboundConnectorFunction { + private static final String PARAM_LANGUAGE = "language"; + private static final String PARAM_SCRIPT_FORMAT = "scriptFormat"; + private static final String PARAM_HEADER = "script"; + + private final ScriptEvaluator scriptEvaluator = new ScriptEvaluator(); + + @Override + public Object execute(OutboundConnectorContext outboundConnectorContext) throws Exception { + // do not leave behind the old party + final Map customHeaders = + outboundConnectorContext.getJobContext().getCustomHeaders(); + final String language = getLanguage(customHeaders); + final String script = getScript(customHeaders); + + final Map variables = getVariablesAsMap(outboundConnectorContext); + + return scriptEvaluator.evaluate(language, script, variables); + } + + private String getLanguage(Map customHeaders) { + return ofNullable(customHeaders.get(PARAM_SCRIPT_FORMAT)) + .orElseGet( + () -> + ofNullable(customHeaders.get(PARAM_LANGUAGE)) + .orElseThrow( + () -> + new RuntimeException( + String.format( + "Missing required custom header '%s'", PARAM_LANGUAGE)))); + } + + private String getScript(Map customHeaders) { + return ofNullable(customHeaders.get(PARAM_HEADER)) + .orElseThrow( + () -> + new RuntimeException( + String.format("Missing required custom header '%s'", PARAM_HEADER))); + } + + private Map getVariablesAsMap(OutboundConnectorContext outboundConnectorContext) { + return (Map) outboundConnectorContext.bindVariables(Map.class); + } +} diff --git a/src/main/java/io/zeebe/script/ScriptEvaluator.java b/connector/src/main/java/io/camunda/community/connector/script/ScriptEvaluator.java similarity index 56% rename from src/main/java/io/zeebe/script/ScriptEvaluator.java rename to connector/src/main/java/io/camunda/community/connector/script/ScriptEvaluator.java index c2a53f0..962c023 100644 --- a/src/main/java/io/zeebe/script/ScriptEvaluator.java +++ b/connector/src/main/java/io/camunda/community/connector/script/ScriptEvaluator.java @@ -13,34 +13,46 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.zeebe.script; +package io.camunda.community.connector.script; +import io.camunda.community.connector.script.spi.ScriptEvaluatorExtension; import java.util.HashMap; import java.util.Map; +import java.util.Set; import javax.script.Bindings; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; -import org.springframework.stereotype.Component; -@Component public class ScriptEvaluator { private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); - private final Map additionalEvaluators = - Map.of("mustache", new MustacheEvaluator()); - private final Map cachedScriptEngines = new HashMap<>(); - private final GraalEvaluator graalEvaluator = new GraalEvaluator(); - private final ScriptEngineEvaluator scriptEngineEvaluator = new ScriptEngineEvaluator(); + private final Map scriptEvaluatorExtensions = new HashMap<>(); + + public ScriptEvaluator() { + ScriptEvaluatorExtension.load() + .forEach( + e -> + e.getEvaluatedLanguage() + .forEach(language -> scriptEvaluatorExtensions.put(language, e))); + } + + public ScriptEvaluator(Set extensions) { + this(); + extensions.forEach( + e -> + e.getEvaluatedLanguage() + .forEach(language -> scriptEvaluatorExtensions.put(language, e))); + } public Object evaluate(String language, String script, Map variables) { - if (additionalEvaluators.containsKey(language)) { - final var scriptEvaluator = additionalEvaluators.get(language); - return scriptEvaluator.eval(script, variables); + if (scriptEvaluatorExtensions.containsKey(language)) { + final var scriptEvaluator = scriptEvaluatorExtensions.get(language); + return scriptEvaluator.evaluateScript(script, variables); } return evalWithScriptEngine(language, script, variables); @@ -49,24 +61,21 @@ public Object evaluate(String language, String script, Map varia private Object evalWithScriptEngine( String language, String script, Map variables) { try { - if (GraalEvaluator.SUPPORTED_LANGUAGES.contains(language)) { - return graalEvaluator.evaluate(language, script, variables); - } else { - 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); - } - return eval(scriptEngine, script, variables); + 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); } + return eval(scriptEngine, script, variables); } catch (Exception e) { final String msg = String.format("Failed to evaluate script '%s' (%s)", script, language); throw new RuntimeException(msg, e); } } - private Object eval(ScriptEngine scriptEngine, String script, Map variables) + private synchronized Object eval( + ScriptEngine scriptEngine, String script, Map variables) throws ScriptException { final ScriptContext context = scriptEngine.getContext(); diff --git a/connector/src/main/java/io/camunda/community/connector/script/ScriptResourceProvider.java b/connector/src/main/java/io/camunda/community/connector/script/ScriptResourceProvider.java new file mode 100644 index 0000000..61db700 --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/ScriptResourceProvider.java @@ -0,0 +1,76 @@ +package io.camunda.community.connector.script; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.util.function.Function; + +public class ScriptResourceProvider { + private final ResourceLoader[] resourceLoaders = + new ResourceLoader[] { + new ResourceLoader("classpath", this::loadFromClassPath), + new ResourceLoader("file", this::loadFromFile) + }; + + public String provideScript(String scriptResource) { + if (scriptResource == null) { + throw new IllegalStateException("scriptResource must not be null"); + } + for (ResourceLoader loader : resourceLoaders) { + if (is(loader.identifier(), scriptResource)) { + return loader + .strategy() + .apply(scriptResource.substring(loader.identifier().length() + 1).trim()); + } + } + return loadFromUrl(scriptResource); + } + + private boolean is(String resourceType, String scriptResource) { + return scriptResource.startsWith(resourceType + ":"); + } + + private String loadFromClassPath(String scriptResource) { + try (InputStream in = this.getClass().getClassLoader().getResourceAsStream(scriptResource)) { + if (in == null) { + throw new NullPointerException(String.format("No resource found for '%s'", scriptResource)); + } + return new String(in.readAllBytes()); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "An exception happened while loading resource '%s' from the classpath", + scriptResource), + e); + } + } + + private String loadFromFile(String scriptResource) { + try { + return new String(Files.readAllBytes(new File(scriptResource).toPath())); + } catch (Exception e) { + throw new RuntimeException( + String.format( + "An exception happened while loading resource '%s' from the file system", + scriptResource), + e); + } + } + + private String loadFromUrl(String scriptResource) { + try { + URL scriptUrl = new URL(scriptResource); + try (InputStream in = scriptUrl.openStream()) { + return new String(in.readAllBytes()); + } + } catch (Exception e) { + throw new RuntimeException( + String.format( + "An exception happened while loading resource '%s' from a URL", scriptResource), + e); + } + } + + private record ResourceLoader(String identifier, Function strategy) {} +} diff --git a/connector/src/main/java/io/camunda/community/connector/script/spi/LanguageProviderExtension.java b/connector/src/main/java/io/camunda/community/connector/script/spi/LanguageProviderExtension.java new file mode 100644 index 0000000..018ee4f --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/spi/LanguageProviderExtension.java @@ -0,0 +1,14 @@ +package io.camunda.community.connector.script.spi; + +import java.util.List; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; + +public interface LanguageProviderExtension { + static List load() { + return ServiceLoader.load(LanguageProviderExtension.class).stream().map(Provider::get).toList(); + } + + Properties getLanguages(); +} diff --git a/connector/src/main/java/io/camunda/community/connector/script/spi/ScriptEvaluatorExtension.java b/connector/src/main/java/io/camunda/community/connector/script/spi/ScriptEvaluatorExtension.java new file mode 100644 index 0000000..53889f2 --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/spi/ScriptEvaluatorExtension.java @@ -0,0 +1,17 @@ +package io.camunda.community.connector.script.spi; + +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; +import java.util.Set; + +public interface ScriptEvaluatorExtension { + static List load() { + return ServiceLoader.load(ScriptEvaluatorExtension.class).stream().map(Provider::get).toList(); + } + + Set getEvaluatedLanguage(); + + Object evaluateScript(String script, Map context); +} diff --git a/connector/src/main/java/io/camunda/community/connector/script/spi/impl/DefaultLanguageProviderExtension.java b/connector/src/main/java/io/camunda/community/connector/script/spi/impl/DefaultLanguageProviderExtension.java new file mode 100644 index 0000000..9aab06e --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/spi/impl/DefaultLanguageProviderExtension.java @@ -0,0 +1,29 @@ +package io.camunda.community.connector.script.spi.impl; + +import io.camunda.community.connector.script.spi.LanguageProviderExtension; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class DefaultLanguageProviderExtension implements LanguageProviderExtension { + private static final String RESOURCE = "script-resource-extensions.properties"; + private Properties properties; + + @Override + public Properties getLanguages() { + if (properties == null) { + loadProperties(); + } + return properties; + } + + private void loadProperties() { + properties = new Properties(); + try (InputStream in = getClass().getClassLoader().getResourceAsStream(RESOURCE)) { + properties.load(in); + } catch (IOException e) { + throw new RuntimeException( + String.format("Unable to load '%s' from the classpath", RESOURCE), e); + } + } +} diff --git a/connector/src/main/java/io/camunda/community/connector/script/spi/impl/MustacheEvaluatorExtension.java b/connector/src/main/java/io/camunda/community/connector/script/spi/impl/MustacheEvaluatorExtension.java new file mode 100644 index 0000000..24a821e --- /dev/null +++ b/connector/src/main/java/io/camunda/community/connector/script/spi/impl/MustacheEvaluatorExtension.java @@ -0,0 +1,20 @@ +package io.camunda.community.connector.script.spi.impl; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; +import io.camunda.community.connector.script.spi.ScriptEvaluatorExtension; +import java.util.Map; +import java.util.Set; + +public class MustacheEvaluatorExtension implements ScriptEvaluatorExtension { + @Override + public Set getEvaluatedLanguage() { + return Set.of("mustache"); + } + + @Override + public Object evaluateScript(String script, Map context) { + final Template template = Mustache.compiler().compile(script); + return template.execute(context); + } +} diff --git a/connector/src/main/resources/META-INF/services/io.camunda.community.connector.script.spi.LanguageProviderExtension b/connector/src/main/resources/META-INF/services/io.camunda.community.connector.script.spi.LanguageProviderExtension new file mode 100644 index 0000000..06a77e5 --- /dev/null +++ b/connector/src/main/resources/META-INF/services/io.camunda.community.connector.script.spi.LanguageProviderExtension @@ -0,0 +1 @@ +io.camunda.community.connector.script.spi.impl.DefaultLanguageProviderExtension \ No newline at end of file diff --git a/connector/src/main/resources/META-INF/services/io.camunda.community.connector.script.spi.ScriptEvaluatorExtension b/connector/src/main/resources/META-INF/services/io.camunda.community.connector.script.spi.ScriptEvaluatorExtension new file mode 100644 index 0000000..0e24ab9 --- /dev/null +++ b/connector/src/main/resources/META-INF/services/io.camunda.community.connector.script.spi.ScriptEvaluatorExtension @@ -0,0 +1 @@ +io.camunda.community.connector.script.spi.impl.MustacheEvaluatorExtension \ No newline at end of file diff --git a/connector/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction b/connector/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction new file mode 100644 index 0000000..62dcd73 --- /dev/null +++ b/connector/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction @@ -0,0 +1,2 @@ +io.camunda.community.connector.script.ScriptConnector +io.camunda.community.connector.script.ScriptConnectorLegacy \ No newline at end of file diff --git a/connector/src/main/resources/script-resource-extensions.properties b/connector/src/main/resources/script-resource-extensions.properties new file mode 100644 index 0000000..9ce05b2 --- /dev/null +++ b/connector/src/main/resources/script-resource-extensions.properties @@ -0,0 +1,11 @@ +# javascript +js=javascript +# groovy +groovy=groovy +gvy=groovy +gy=groovy +gsh=groovy +# mustache +mustache=mustache +# kotlin +kt=kotlin \ No newline at end of file diff --git a/src/test/java/io/zeebe/script/EvaluationGroovyTest.java b/connector/src/test/java/io/camunda/community/connector/script/EvaluationGroovyTest.java similarity index 98% rename from src/test/java/io/zeebe/script/EvaluationGroovyTest.java rename to connector/src/test/java/io/camunda/community/connector/script/EvaluationGroovyTest.java index 6c557a4..1bf7acc 100644 --- a/src/test/java/io/zeebe/script/EvaluationGroovyTest.java +++ b/connector/src/test/java/io/camunda/community/connector/script/EvaluationGroovyTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.zeebe.script; +package io.camunda.community.connector.script; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; diff --git a/src/test/java/io/zeebe/script/EvaluationJavaScriptTest.java b/connector/src/test/java/io/camunda/community/connector/script/EvaluationJavaScriptTest.java similarity index 98% rename from src/test/java/io/zeebe/script/EvaluationJavaScriptTest.java rename to connector/src/test/java/io/camunda/community/connector/script/EvaluationJavaScriptTest.java index 6f8ea60..d67d438 100644 --- a/src/test/java/io/zeebe/script/EvaluationJavaScriptTest.java +++ b/connector/src/test/java/io/camunda/community/connector/script/EvaluationJavaScriptTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.zeebe.script; +package io.camunda.community.connector.script; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; diff --git a/src/test/java/io/zeebe/script/EvaluationKotlinTest.java b/connector/src/test/java/io/camunda/community/connector/script/EvaluationKotlinTest.java similarity index 97% rename from src/test/java/io/zeebe/script/EvaluationKotlinTest.java rename to connector/src/test/java/io/camunda/community/connector/script/EvaluationKotlinTest.java index b22a98e..e9e284f 100644 --- a/src/test/java/io/zeebe/script/EvaluationKotlinTest.java +++ b/connector/src/test/java/io/camunda/community/connector/script/EvaluationKotlinTest.java @@ -1,4 +1,4 @@ -package io.zeebe.script; +package io.camunda.community.connector.script; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; diff --git a/src/test/java/io/zeebe/script/EvaluationMustacheTest.java b/connector/src/test/java/io/camunda/community/connector/script/EvaluationMustacheTest.java similarity index 97% rename from src/test/java/io/zeebe/script/EvaluationMustacheTest.java rename to connector/src/test/java/io/camunda/community/connector/script/EvaluationMustacheTest.java index 2a4cfeb..e54e4b3 100644 --- a/src/test/java/io/zeebe/script/EvaluationMustacheTest.java +++ b/connector/src/test/java/io/camunda/community/connector/script/EvaluationMustacheTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.zeebe.script; +package io.camunda.community.connector.script; import static org.assertj.core.api.Assertions.assertThat; diff --git a/connector/src/test/java/io/camunda/community/connector/script/LanguageProviderTest.java b/connector/src/test/java/io/camunda/community/connector/script/LanguageProviderTest.java new file mode 100644 index 0000000..e0b93b7 --- /dev/null +++ b/connector/src/test/java/io/camunda/community/connector/script/LanguageProviderTest.java @@ -0,0 +1,14 @@ +package io.camunda.community.connector.script; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class LanguageProviderTest { + @Test + void shouldReturnLanguage() { + LanguageProvider provider = new LanguageProvider(); + String languageForScriptResource = provider.getLanguageForScriptResource("some-resource.js"); + assertThat(languageForScriptResource).isEqualTo("javascript"); + } +} diff --git a/src/test/java/io/zeebe/script/ScriptEvaluatorTest.java b/connector/src/test/java/io/camunda/community/connector/script/ScriptEvaluatorTest.java similarity index 86% rename from src/test/java/io/zeebe/script/ScriptEvaluatorTest.java rename to connector/src/test/java/io/camunda/community/connector/script/ScriptEvaluatorTest.java index aa3f8a3..f4818e5 100644 --- a/src/test/java/io/zeebe/script/ScriptEvaluatorTest.java +++ b/connector/src/test/java/io/camunda/community/connector/script/ScriptEvaluatorTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.zeebe.script; +package io.camunda.community.connector.script; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -39,13 +39,6 @@ public void shouldEvaluateGroovy() { assertThat(result).isEqualTo(123); } - @Test - public void shouldEvaluateFeel() { - final Object result = scriptEvaluator.evaluate("feel", "123", Collections.emptyMap()); - - assertThat(result).isEqualTo(123L); - } - @Test public void shouldEvaluateKotlin() { final Object result = scriptEvaluator.evaluate("kotlin", "123", Collections.emptyMap()); @@ -71,14 +64,6 @@ public void shouldEvaluateGroovyWithVariables() { assertThat(result).isEqualTo(123); } - @Test - public void shouldEvaluateFeelWithVariables() { - - final Object result = scriptEvaluator.evaluate("feel", "a", Collections.singletonMap("a", 123)); - - assertThat(result).isEqualTo(123L); - } - @Test public void shouldEvaluateKotlinWithVariables() { final Object result = diff --git a/connector/src/test/java/io/camunda/community/connector/script/ScriptResourceProviderTest.java b/connector/src/test/java/io/camunda/community/connector/script/ScriptResourceProviderTest.java new file mode 100644 index 0000000..73956aa --- /dev/null +++ b/connector/src/test/java/io/camunda/community/connector/script/ScriptResourceProviderTest.java @@ -0,0 +1,29 @@ +package io.camunda.community.connector.script; + +import static org.assertj.core.api.Assertions.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class ScriptResourceProviderTest { + @Test + void shouldLoadClasspathResource() { + String scriptResource = "classpath:test-script.js"; + ScriptResourceProvider resourceProvider = new ScriptResourceProvider(); + String script = resourceProvider.provideScript(scriptResource); + assertThat(script).isEqualTo("a + b;"); + } + + @Test + void shouldLoadFileResource(@TempDir File directory) throws IOException { + File scriptFile = new File(directory, "test-script.js"); + Files.writeString(scriptFile.toPath(), "a + b;"); + String scriptResource = "file:" + scriptFile; + ScriptResourceProvider resourceProvider = new ScriptResourceProvider(); + String script = resourceProvider.provideScript(scriptResource); + assertThat(script).isEqualTo("a + b;"); + } +} diff --git a/src/test/java/io/zeebe/script/WorkflowTest.java b/connector/src/test/java/io/camunda/community/connector/script/WorkflowTest.java similarity index 74% rename from src/test/java/io/zeebe/script/WorkflowTest.java rename to connector/src/test/java/io/camunda/community/connector/script/WorkflowTest.java index 84f71fe..7562a86 100644 --- a/src/test/java/io/zeebe/script/WorkflowTest.java +++ b/connector/src/test/java/io/camunda/community/connector/script/WorkflowTest.java @@ -1,5 +1,6 @@ -package io.zeebe.script; +package io.camunda.community.connector.script; +import static io.camunda.community.connector.script.ScriptConnector.*; import static org.assertj.core.api.Assertions.assertThat; import io.camunda.zeebe.client.ZeebeClient; @@ -9,6 +10,7 @@ import io.camunda.zeebe.spring.test.ZeebeSpringTest; import java.util.Collections; import java.util.Map; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -30,7 +32,8 @@ public void shouldReturnResult() { t -> t.zeebeJobType("script") .zeebeTaskHeader("language", "groovy") - .zeebeTaskHeader("script", "x + 1")) + .zeebeTaskHeader("script", "x + 1") + .zeebeTaskHeader("resultVariable", "result")) .done(); final var workflowInstanceResult = @@ -40,6 +43,29 @@ public void shouldReturnResult() { } @Test + void shouldReturnResultConnector() { + BpmnModelInstance modelInstance = + Bpmn.createExecutableProcess("process") + .startEvent() + .scriptTask( + "task", + t -> + t.zeebeJobType(SCRIPT_CONNECTOR_TYPE) + .zeebeInput("={a:a,b:a}", "context") + .zeebeInput("a+b", "script.embedded") + .zeebeInput("embedded", "script.type") + .zeebeInput("javascript", "script.language") + .zeebeTaskHeader("resultVariable", "result")) + .endEvent() + .done(); + final var workflowInstanceResult = + deployAndCreateInstance(modelInstance, Collections.singletonMap("a", 3)); + + assertThat(workflowInstanceResult.getVariablesAsMap()).containsEntry("result", 6); + } + + @Test + @Disabled public void shouldGetCurrentJob() { final BpmnModelInstance workflow = @@ -60,6 +86,7 @@ public void shouldGetCurrentJob() { } @Test + @Disabled public void shouldUseZeebeClient() { final String groovyScript = "zeebeClient.newPublishMessageCommand()" diff --git a/src/main/java/io/zeebe/script/ZeebeScriptWorkerApplication.java b/connector/src/test/java/io/camunda/community/connector/script/ZeebeScriptWorkerApplication.java similarity index 90% rename from src/main/java/io/zeebe/script/ZeebeScriptWorkerApplication.java rename to connector/src/test/java/io/camunda/community/connector/script/ZeebeScriptWorkerApplication.java index 8b3d8ec..2eb5212 100644 --- a/src/main/java/io/zeebe/script/ZeebeScriptWorkerApplication.java +++ b/connector/src/test/java/io/camunda/community/connector/script/ZeebeScriptWorkerApplication.java @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.zeebe.script; +package io.camunda.community.connector.script; -import io.camunda.zeebe.spring.client.EnableZeebeClient; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -@EnableZeebeClient public class ZeebeScriptWorkerApplication { public static void main(String[] args) { diff --git a/connector/src/test/resources/application.yaml b/connector/src/test/resources/application.yaml new file mode 100644 index 0000000..d28a711 --- /dev/null +++ b/connector/src/test/resources/application.yaml @@ -0,0 +1,7 @@ +camunda.operate.enabled: false +camunda: + connector: + webhook: + enabled: false + polling: + enabled: false \ No newline at end of file diff --git a/connector/src/test/resources/test-script.js b/connector/src/test/resources/test-script.js new file mode 100644 index 0000000..b321213 --- /dev/null +++ b/connector/src/test/resources/test-script.js @@ -0,0 +1 @@ +a + b; \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 47f892b..e9a1d21 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -6,8 +6,8 @@ networks: services: zeebe: - container_name: zeebe_broker - image: camunda/zeebe:8.1.5 + container_name: zeebe + image: camunda/zeebe:8.3.0 environment: - ZEEBE_LOG_LEVEL=debug ports: @@ -15,12 +15,30 @@ services: - "9600:9600" networks: - zeebe_network - zeebe-script-worker: - container_name: zeebe-script-worker - image: ghcr.io/camunda-community-hub/zeebe-script-worker:1.2.0 + script-connector-runtime: + container_name: script-connector-runtime + image: ghcr.io/camunda-community-hub/script-connector/runtime:latest environment: - - zeebe.client.broker.contactPoint=zeebe:26500 + - ZEEBE_CLIENT_BROKER_GATEWAY-ADDRESS=zeebe:26500 + - ZEEBE_CLIENT_SECURITY_PLAINTEXT=true depends_on: - zeebe networks: - zeebe_network + script-connector-bundled: + container_name: script-connector-bundled + image: camunda/connectors-bundle:8.3.0 + environment: + - ZEEBE_CLIENT_BROKER_GATEWAY-ADDRESS=zeebe:26500 + - ZEEBE_CLIENT_SECURITY_PLAINTEXT=true + - CAMUNDA_CONNECTOR_POLLING_ENABLED=false + - CAMUNDA_CONNECTOR_WEBHOOK_ENABLED=false + - SPRING_MAIN_WEB-APPLICATION-TYPE=none + - OPERATE_CLIENT_ENABLED=false + depends_on: + - zeebe + networks: + - zeebe_network + volumes: + - ./../connector/target/script-connector-1.2.1-SNAPSHOT-shaded.jar:/opt/custom/script-connector.jar + diff --git a/pom.xml b/pom.xml index 38ed426..4307e51 100644 --- a/pom.xml +++ b/pom.xml @@ -3,11 +3,15 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - Zeebe Script Worker - io.zeebe - zeebe-script-worker + Script Connector Parent + + connector + runtime + + io.camunda.connectors.community + script-connector-parent 1.2.1-SNAPSHOT - jar + pom org.camunda.community @@ -26,9 +30,10 @@ ${java.version} ${java.version} - 3.1.5 - 8.2.16 - 8.2.4 + 3.1.5 + 8.3.0 + 8.3.0 + 8.3.0 2.4.21 1.9.10 @@ -39,29 +44,28 @@ 3.1.1 3.3.1 3.5.1 - 3.2.1 + 3.1.2 2.40.0 3.11.0 0.9.28 - 3.6.1 + 3.6.0 3.12.1 3.6.0 3.0.1 1.14.2 3.3.0 + 3.3.0 - io.camunda zeebe-bom - ${version.zeebe} + ${version.camunda} import pom - org.jetbrains.kotlin kotlin-bom @@ -69,107 +73,69 @@ pom import - org.springframework.boot spring-boot-dependencies - ${version.spring.boot} + ${version.spring-boot} pom import - + + io.camunda.connector + connector-core + ${version.camunda-connectors} + + + io.camunda.connector + element-template-generator + ${version.camunda-connectors} + + + io.camunda.spring + spring-boot-starter-camunda-test + ${version.camunda-spring} + + + io.camunda.connector + spring-boot-starter-camunda-connectors + ${version.camunda-connectors} + + + org.codehaus.groovy + groovy-all + ${version.groovy} + + + org.graalvm.js + js + ${version.graalvm} + + + org.graalvm.js + js-scriptengine + ${version.graalvm} + + + org.jetbrains.kotlin + kotlin-scripting-jsr223 + ${version.kotlin} + + + io.camunda.connectors.community + script-connector + ${project.version} + - - - - - io.camunda - spring-zeebe-starter - ${version.zeebe.spring} - - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-mustache - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - org.codehaus.groovy - groovy-all - ${version.groovy} - - - - org.graalvm.sdk - graal-sdk - ${version.graalvm} - - - - org.graalvm.js - js - ${version.graalvm} - - - - org.jetbrains.kotlin - kotlin-scripting-jsr223 - ${version.kotlin} - - - - - org.junit.jupiter - junit-jupiter - 5.10.0 - test - - - - org.assertj - assertj-core - 3.24.2 - test - - - - org.testcontainers - junit-jupiter - 1.19.1 - test - - - - io.camunda - spring-zeebe-test - ${version.zeebe.spring} - test - - - - + + io.camunda.connector + element-template-generator-maven-plugin + ${version.camunda-connectors} + org.sonatype.plugins nexus-staging-maven-plugin @@ -259,7 +225,7 @@ - + @@ -281,8 +247,8 @@ *.md .gitignore - - + + true 2 @@ -290,9 +256,9 @@ - + - + @@ -327,7 +293,7 @@ - + @@ -336,67 +302,6 @@ - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.1 - - - - org.apache.maven.plugins - maven-javadoc-plugin - - ${version.java} - - - - - org.springframework.boot - spring-boot-maven-plugin - ${version.spring.boot} - - - - repackage - - - - - - - - org.jetbrains.kotlin - kotlin-compiler-embeddable - - - - - - - com.google.cloud.tools - jib-maven-plugin - 3.4.0 - - - deploy - - build - - - - - - ghcr.io/camunda-community-hub/zeebe-script-worker - ${project.version} - - - - - @@ -447,38 +352,4 @@ - - - - zeebe - Zeebe Repository - https://artifacts.camunda.com/artifactory/zeebe-io/ - - true - - - false - - - - - zeebe-snapshots - Zeebe Snapshot Repository - https://artifacts.camunda.com/artifactory/zeebe-io-snapshots/ - - false - - - true - - - - - - https://github.com/camunda-community-hub/zeebe-script-worker - scm:git:git@github.com:camunda-community-hub/zeebe-script-worker.git - scm:git:git@github.com:camunda-community-hub/zeebe-script-worker.git - HEAD - - diff --git a/runtime/pom.xml b/runtime/pom.xml new file mode 100644 index 0000000..67604c3 --- /dev/null +++ b/runtime/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + io.camunda.connectors.community + script-connector-parent + 1.2.1-SNAPSHOT + + + script-connector-runtime + Script Connector Runtime + + + + io.camunda.connectors.community + script-connector + + + io.camunda.connector + spring-boot-starter-camunda-connectors + + + io.camunda.spring + spring-boot-starter-camunda-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + + + + + + + + + + build-image + + + + org.springframework.boot + spring-boot-maven-plugin + + + build-version + + build-image + + package + + + ghcr.io/camunda-community-hub/script-connector/runtime:${project.version} + + + + + + build-latest + + build-image + + package + + + ghcr.io/camunda-community-hub/script-connector/runtime + + + + + + + + + + + \ No newline at end of file diff --git a/runtime/src/main/java/io/camunda/community/connector/script/App.java b/runtime/src/main/java/io/camunda/community/connector/script/App.java new file mode 100644 index 0000000..3ecb93a --- /dev/null +++ b/runtime/src/main/java/io/camunda/community/connector/script/App.java @@ -0,0 +1,11 @@ +package io.camunda.community.connector.script; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/runtime/src/main/resources/application.yaml b/runtime/src/main/resources/application.yaml new file mode 100644 index 0000000..f3d8fd1 --- /dev/null +++ b/runtime/src/main/resources/application.yaml @@ -0,0 +1,7 @@ +camunda: + connector: + polling: + enabled: false + webhook: + enabled: false + diff --git a/runtime/src/test/java/io/camunda/community/connector/script/AppTest.java b/runtime/src/test/java/io/camunda/community/connector/script/AppTest.java new file mode 100644 index 0000000..8bf4aed --- /dev/null +++ b/runtime/src/test/java/io/camunda/community/connector/script/AppTest.java @@ -0,0 +1,63 @@ +package io.camunda.community.connector.script; + +import static io.camunda.community.connector.script.ScriptConnector.*; +import static org.assertj.core.api.Assertions.*; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ProcessInstanceResult; +import io.camunda.zeebe.model.bpmn.Bpmn; +import io.camunda.zeebe.model.bpmn.BpmnModelInstance; +import io.camunda.zeebe.spring.test.ZeebeSpringTest; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@ZeebeSpringTest +public class AppTest { + + @Autowired ZeebeClient zeebeClient; + + @Test + void shouldRun() { + // just to assert it runs + } + + @Test + void shouldExecuteConnector() { + BpmnModelInstance modelInstance = + Bpmn.createExecutableProcess("process") + .startEvent() + .scriptTask( + "task", + t -> + t.zeebeJobType(SCRIPT_CONNECTOR_TYPE) + .zeebeInput("={a:a,b:a}", "context") + .zeebeInput("a+b", "script.embedded") + .zeebeInput("embedded", "script.type") + .zeebeInput("javascript", "script.language") + .zeebeTaskHeader("resultVariable", "result")) + .endEvent() + .done(); + final var workflowInstanceResult = + deployAndCreateInstance(modelInstance, Collections.singletonMap("a", 3)); + + assertThat(workflowInstanceResult.getVariablesAsMap()).containsEntry("result", 6); + } + + private ProcessInstanceResult deployAndCreateInstance( + final BpmnModelInstance workflow, Map variables) { + zeebeClient.newDeployResourceCommand().addProcessModel(workflow, "process.bpmn").send().join(); + + return zeebeClient + .newCreateInstanceCommand() + .bpmnProcessId("process") + .latestVersion() + .variables(variables) + .withResult() + .send() + .join(); + } +} diff --git a/src/main/java/io/zeebe/script/GraalEvaluator.java b/src/main/java/io/zeebe/script/GraalEvaluator.java deleted file mode 100644 index 6a9daa2..0000000 --- a/src/main/java/io/zeebe/script/GraalEvaluator.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright © 2017 camunda services GmbH (info@camunda.com) - * - * 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 SUPPORTED_LANGUAGES = Arrays.asList("js", "javascript"); - - public Object evaluate(String language, String script, Map 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(); - } -} diff --git a/src/main/java/io/zeebe/script/MustacheEvaluator.java b/src/main/java/io/zeebe/script/MustacheEvaluator.java deleted file mode 100644 index 648739e..0000000 --- a/src/main/java/io/zeebe/script/MustacheEvaluator.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.zeebe.script; - -import com.samskivert.mustache.Mustache; -import com.samskivert.mustache.Template; -import java.util.Map; - -public final class MustacheEvaluator implements ZeebeScriptEvaluator { - - public Object eval(String script, Map context) { - final Template template = Mustache.compiler().compile(script); - return template.execute(context); - } -} diff --git a/src/main/java/io/zeebe/script/ScriptEngineEvaluator.java b/src/main/java/io/zeebe/script/ScriptEngineEvaluator.java deleted file mode 100644 index 40c22e5..0000000 --- a/src/main/java/io/zeebe/script/ScriptEngineEvaluator.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2017 camunda services GmbH (info@camunda.com) - * - * 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 cachedScriptEngines = new HashMap<>(); - - public Object evaluate(String language, String script, Map 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); - } -} diff --git a/src/main/java/io/zeebe/script/ScriptJobHandler.java b/src/main/java/io/zeebe/script/ScriptJobHandler.java deleted file mode 100644 index 6453e0d..0000000 --- a/src/main/java/io/zeebe/script/ScriptJobHandler.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright © 2017 camunda services GmbH (info@camunda.com) - * - * 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 io.camunda.zeebe.client.ZeebeClient; -import io.camunda.zeebe.client.api.response.ActivatedJob; -import io.camunda.zeebe.client.api.worker.JobClient; -import io.camunda.zeebe.client.api.worker.JobHandler; -import io.camunda.zeebe.spring.client.annotation.ZeebeWorker; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class ScriptJobHandler implements JobHandler { - - private static final String HEADER_LANGUAGE = "language"; - private static final String HEADER_SCRIPT = "script"; - - private final ScriptEvaluator scriptEvaluator = new ScriptEvaluator(); - - private final ZeebeClient zeebeClient; - - @Autowired - public ScriptJobHandler(ZeebeClient zeebeClient) { - this.zeebeClient = zeebeClient; - } - - @Override - @ZeebeWorker - public void handle(JobClient jobClient, ActivatedJob job) { - - final Map customHeaders = job.getCustomHeaders(); - final String language = getLanguage(customHeaders); - final String script = getScript(customHeaders); - - final Map variables = job.getVariablesAsMap(); - - // add context - variables.put("job", job); - variables.put("zeebeClient", zeebeClient); - - final Object result = scriptEvaluator.evaluate(language, script, variables); - - jobClient - .newCompleteCommand(job.getKey()) - .variables(Collections.singletonMap("result", result)) - .send(); - } - - private String getLanguage(Map customHeaders) { - return Optional.ofNullable(customHeaders.get(HEADER_LANGUAGE)) - .orElseThrow( - () -> - new RuntimeException( - String.format("Missing required custom header '%s'", HEADER_LANGUAGE))); - } - - private String getScript(Map customHeaders) { - return Optional.ofNullable(customHeaders.get(HEADER_SCRIPT)) - .orElseThrow( - () -> - new RuntimeException( - String.format("Missing required custom header '%s'", HEADER_SCRIPT))); - } -} diff --git a/src/main/java/io/zeebe/script/ZeebeScriptEvaluator.java b/src/main/java/io/zeebe/script/ZeebeScriptEvaluator.java deleted file mode 100644 index a018863..0000000 --- a/src/main/java/io/zeebe/script/ZeebeScriptEvaluator.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.zeebe.script; - -import java.util.Map; - -@FunctionalInterface -public interface ZeebeScriptEvaluator { - - Object eval(String script, Map context); -} diff --git a/src/main/resources/META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper b/src/main/resources/META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper deleted file mode 100644 index 406b0de..0000000 --- a/src/main/resources/META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper +++ /dev/null @@ -1 +0,0 @@ -org.camunda.feel.impl.JavaValueMapper \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml deleted file mode 100644 index 30aafd4..0000000 --- a/src/main/resources/application.yaml +++ /dev/null @@ -1,16 +0,0 @@ -zeebe: - client: - worker: - defaultName: script-worker - defaultType: script - threads: 3 - - job.timeout: 10000 - broker.contactPoint: 127.0.0.1:26500 - security.plaintext: true - -logging: - level: - root: ERROR - io.zeebe: INFO - io.zeebe.script: DEBUG diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt deleted file mode 100644 index d532a9b..0000000 --- a/src/main/resources/banner.txt +++ /dev/null @@ -1,8 +0,0 @@ - -___ __ - _/ _ _ |_ _ (_ _ _ . _ |_ | | _ _ | _ _ -/__ (- (- |_) (- __) (_ | | |_) |_ |/\| (_) | |( (- | - | - -============================================================= - ${application.formatted-version}