From abf8e115eb27d480fd8498f1785bd97b6ca796bf Mon Sep 17 00:00:00 2001 From: fjtirado Date: Tue, 11 Nov 2025 14:37:09 +0100 Subject: [PATCH] Structure changes Signed-off-by: fjtirado --- .../impl/executors/RunScriptExecutor.java | 276 ++++-------------- .../impl/scripts/ScriptContext.java | 26 ++ .../impl/scripts/ScriptLanguageId.java | 47 +++ .../ScriptRunner.java} | 24 +- .../script/js/JavaScriptScriptTaskRunner.java | 140 ++++----- ...verlessworkflow.impl.scripts.ScriptRunner} | 0 6 files changed, 214 insertions(+), 299 deletions(-) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptContext.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptLanguageId.java rename impl/core/src/main/java/io/serverlessworkflow/impl/{executors/ScriptTaskRunner.java => scripts/ScriptRunner.java} (63%) rename impl/script-js/src/main/resources/META-INF/services/{io.serverlessworkflow.impl.executors.ScriptTaskRunner => io.serverlessworkflow.impl.scripts.ScriptRunner} (100%) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java index 9ed8222a..1f456e10 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RunScriptExecutor.java @@ -23,263 +23,105 @@ import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; -import io.serverlessworkflow.impl.WorkflowError; -import io.serverlessworkflow.impl.WorkflowException; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; import io.serverlessworkflow.impl.resources.ResourceLoaderUtils; -import java.util.Arrays; -import java.util.HashMap; +import io.serverlessworkflow.impl.scripts.ScriptContext; +import io.serverlessworkflow.impl.scripts.ScriptLanguageId; +import io.serverlessworkflow.impl.scripts.ScriptRunner; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.ServiceLoader; import java.util.concurrent.CompletableFuture; public class RunScriptExecutor implements RunnableTask { - public enum LanguageId { - JS("js"), - PYTHON("python"); + private Optional>> environmentExpr; - private final String lang; + private Optional>> argumentExpr; - LanguageId(String lang) { - this.lang = lang; - } - - public String getLang() { - return lang; - } - - public static boolean isSupported(String lang) { - for (LanguageId l : LanguageId.values()) { - if (l.getLang().equalsIgnoreCase(lang)) { - return true; - } - } - return false; - } - } - - @FunctionalInterface - private interface CodeSupplier { - String apply(WorkflowContext workflowContext, TaskContext taskContext); - } - - @SuppressWarnings("rawtypes") - private Map environmentExpr; - - @SuppressWarnings("rawtypes") - private Map argumentExpr; - - private CodeSupplier codeSupplier; + private WorkflowValueResolver codeSupplier; private boolean isAwait; private RunTaskConfiguration.ProcessReturnType returnType; - private ScriptTaskRunner taskRunner; + private ScriptRunner taskRunner; @Override public void init(RunScript taskConfiguration, WorkflowDefinition definition) { ScriptUnion scriptUnion = taskConfiguration.getScript(); Script script = scriptUnion.get(); - String language = scriptUnion.get().getLanguage(); - - WorkflowApplication application = definition.application(); - if (language == null || !LanguageId.isSupported(language)) { - throw new IllegalArgumentException( - "Unsupported script language: " - + language - + ". Supported languages are: " - + Arrays.toString( - Arrays.stream(LanguageId.values()).map(LanguageId::getLang).toArray())); - } + ScriptLanguageId language = ScriptLanguageId.from(script.getLanguage()); this.taskRunner = - ServiceLoader.load(ScriptTaskRunner.class) + ServiceLoader.load(ScriptRunner.class).stream() + .map(ServiceLoader.Provider::get) + .filter(s -> s.identifier().equals(language)) .findFirst() .orElseThrow( - () -> new IllegalStateException("No implementation found for ScriptTaskRunner")); + () -> + new IllegalStateException( + "No script runner implementation found for language " + language)); this.isAwait = taskConfiguration.isAwait(); this.returnType = taskConfiguration.getReturn(); - if (script.getEnvironment() != null - && script.getEnvironment().getAdditionalProperties() != null) { - this.environmentExpr = - buildMapResolvers(application, script.getEnvironment().getAdditionalProperties()); - } else { - this.environmentExpr = Map.of(); - } - - if (script.getArguments() != null && script.getArguments().getAdditionalProperties() != null) { - this.argumentExpr = - buildMapResolvers(application, script.getArguments().getAdditionalProperties()); - } else { - this.argumentExpr = Map.of(); - } + WorkflowApplication application = definition.application(); + this.environmentExpr = + script.getEnvironment() != null && script.getEnvironment().getAdditionalProperties() != null + ? Optional.of( + WorkflowUtils.buildMapResolver( + application, null, script.getEnvironment().getAdditionalProperties())) + : Optional.empty(); + + this.argumentExpr = + script.getArguments() != null && script.getArguments().getAdditionalProperties() != null + ? Optional.of( + WorkflowUtils.buildMapResolver( + application, null, script.getArguments().getAdditionalProperties())) + : Optional.empty(); this.codeSupplier = - (workflowContext, taskContext) -> { - if (scriptUnion.getInlineScript() != null) { - return scriptUnion.getInlineScript().getCode(); - } else if (scriptUnion.getExternalScript() == null) { - throw new WorkflowException( - WorkflowError.runtime( - taskContext, new IllegalStateException("No script source defined.")) - .build()); - } else { - return definition - .resourceLoader() - .load( - scriptUnion.getExternalScript().getSource(), - ResourceLoaderUtils::readString, - workflowContext, - taskContext, - taskContext.input()); - } - }; + scriptUnion.getInlineScript() != null + ? WorkflowUtils.buildStringFilter(application, scriptUnion.getInlineScript().getCode()) + : (w, t, m) -> + definition + .resourceLoader() + .load( + Objects.requireNonNull( + scriptUnion.getExternalScript(), + "External script is required if inline script was not set") + .getSource(), + ResourceLoaderUtils::readString, + w, + t, + m); } @Override public CompletableFuture apply( WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) { - - RunScriptContext.RunScriptContextBuilder builder = - new RunScriptContext.RunScriptContextBuilder(); - - Map envs = new HashMap<>(); - this.environmentExpr.forEach( - (k, v) -> { - Object resolved = v.apply(workflowContext, taskContext, input); - envs.put(k, resolved.toString()); - }); - - Map args = new HashMap<>(); - this.argumentExpr.forEach( - (k, v) -> { - Object resolved = v.apply(workflowContext, taskContext, input); - args.put(k, resolved); - }); - - String code = this.codeSupplier.apply(workflowContext, taskContext); - - RunScriptContext scriptContext = - builder - .withApplication(workflowContext.definition().application()) - .withReturnType(returnType) - .withCode(code) - .withArguments(args) - .withEnvironment(envs) - .withAwait(isAwait) - .build(); - return CompletableFuture.supplyAsync( - () -> taskRunner.buildRun(taskContext).apply(scriptContext, input)); + () -> + taskRunner.runScript( + new ScriptContext( + argumentExpr + .map(m -> m.apply(workflowContext, taskContext, input)) + .orElse(Map.of()), + environmentExpr + .map(m -> m.apply(workflowContext, taskContext, input)) + .orElse(Map.of()), + codeSupplier.apply(workflowContext, taskContext, input), + isAwait, + returnType), + workflowContext, + taskContext, + input)); } @Override public boolean accept(Class clazz) { return RunScript.class.equals(clazz); } - - /** Builds a map of WorkflowValueResolvers from the provided properties. */ - @SuppressWarnings("rawtypes") - private Map buildMapResolvers( - WorkflowApplication application, Map properties) { - Map resolvers = new HashMap<>(); - if (properties != null) { - for (Map.Entry entry : properties.entrySet()) { - WorkflowValueResolver valueResolver = - WorkflowUtils.buildStringFilter(application, entry.getValue().toString()); - resolvers.put(entry.getKey(), valueResolver); - } - } - return resolvers; - } - - public static class RunScriptContext { - private final WorkflowApplication application; - private final Map args; - private final Map envs; - private final String code; - private final boolean isAwait; - private final RunTaskConfiguration.ProcessReturnType returnType; - - public RunScriptContext(RunScriptContextBuilder builder) { - this.application = builder.application; - this.args = builder.args; - this.envs = builder.envs; - this.code = builder.code; - this.isAwait = builder.awaiting; - this.returnType = builder.returnType; - } - - public Map getArgs() { - return args; - } - - public Map getEnvs() { - return envs; - } - - public String getCode() { - return code; - } - - public boolean isAwait() { - return isAwait; - } - - public WorkflowApplication getApplication() { - return application; - } - - public RunTaskConfiguration.ProcessReturnType getReturnType() { - return returnType; - } - - public static class RunScriptContextBuilder { - private Map args; - private Map envs; - private String code; - private boolean awaiting; - private WorkflowApplication application; - private RunTaskConfiguration.ProcessReturnType returnType; - - public RunScriptContextBuilder withArguments(Map args) { - this.args = args; - return this; - } - - public RunScriptContextBuilder withEnvironment(Map envs) { - this.envs = envs; - return this; - } - - public RunScriptContextBuilder withCode(String code) { - this.code = code; - return this; - } - - public RunScriptContextBuilder withAwait(boolean awaiting) { - this.awaiting = awaiting; - return this; - } - - public RunScriptContextBuilder withApplication(WorkflowApplication application) { - this.application = application; - return this; - } - - public RunScriptContextBuilder withReturnType( - RunTaskConfiguration.ProcessReturnType returnType) { - this.returnType = returnType; - return this; - } - - public RunScriptContext build() { - return new RunScriptContext(this); - } - } - } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptContext.java new file mode 100644 index 00000000..68f6692d --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptContext.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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.serverlessworkflow.impl.scripts; + +import io.serverlessworkflow.api.types.RunTaskConfiguration; +import java.util.Map; + +public record ScriptContext( + Map args, + Map envs, + String code, + boolean isAwait, + RunTaskConfiguration.ProcessReturnType returnType) {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptLanguageId.java b/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptLanguageId.java new file mode 100644 index 00000000..70cf0fb6 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptLanguageId.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * 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.serverlessworkflow.impl.scripts; + +import java.util.Arrays; + +public enum ScriptLanguageId { + JS("js"), + PYTHON("python"); + + private final String lang; + + ScriptLanguageId(String lang) { + this.lang = lang; + } + + public String getLang() { + return lang; + } + + public static ScriptLanguageId from(String lang) { + for (ScriptLanguageId l : ScriptLanguageId.values()) { + if (l.getLang().equalsIgnoreCase(lang)) { + return l; + } + } + throw new IllegalStateException( + "Unsupported script language: " + + lang + + ". Supported languages are: " + + Arrays.toString( + Arrays.stream(ScriptLanguageId.values()).map(ScriptLanguageId::getLang).toArray())); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ScriptTaskRunner.java b/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptRunner.java similarity index 63% rename from impl/core/src/main/java/io/serverlessworkflow/impl/executors/ScriptTaskRunner.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptRunner.java index 361ddcdd..f2795352 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ScriptTaskRunner.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/scripts/ScriptRunner.java @@ -13,29 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl.executors; +package io.serverlessworkflow.impl.scripts; +import io.serverlessworkflow.impl.ServicePriority; import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowModel; -import java.util.function.BiFunction; +import io.serverlessworkflow.impl.executors.RunScriptExecutor; /** Represents a script task that executes a script in a specific scripting language. */ -public interface ScriptTaskRunner { +public interface ScriptRunner extends ServicePriority { /** * The scripting language supported by this script task runner. * * @return the scripting language as {@link RunScriptExecutor.LanguageId} enum. */ - RunScriptExecutor.LanguageId identifier(); + ScriptLanguageId identifier(); - /** - * Returns a function that executes the script task. - * - * @param taskContext the task context for the script task. - * @return a @{@link BiFunction}} that takes a RunScriptContext and a WorkflowModel as input and - * returns a WorkflowModel as output. - */ - BiFunction buildRun( - TaskContext taskContext); + WorkflowModel runScript( + ScriptContext script, + WorkflowContext workflowContext, + TaskContext taskContext, + WorkflowModel input); } diff --git a/impl/script-js/src/main/java/io/serverlessworkflow/impl/executors/script/js/JavaScriptScriptTaskRunner.java b/impl/script-js/src/main/java/io/serverlessworkflow/impl/executors/script/js/JavaScriptScriptTaskRunner.java index d827c61a..12540048 100644 --- a/impl/script-js/src/main/java/io/serverlessworkflow/impl/executors/script/js/JavaScriptScriptTaskRunner.java +++ b/impl/script-js/src/main/java/io/serverlessworkflow/impl/executors/script/js/JavaScriptScriptTaskRunner.java @@ -15,103 +15,105 @@ */ package io.serverlessworkflow.impl.executors.script.js; +import io.serverlessworkflow.api.types.RunTaskConfiguration; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowError; import io.serverlessworkflow.impl.WorkflowException; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowModelFactory; import io.serverlessworkflow.impl.executors.ProcessResult; -import io.serverlessworkflow.impl.executors.RunScriptExecutor; -import io.serverlessworkflow.impl.executors.ScriptTaskRunner; +import io.serverlessworkflow.impl.scripts.ScriptContext; +import io.serverlessworkflow.impl.scripts.ScriptLanguageId; +import io.serverlessworkflow.impl.scripts.ScriptRunner; import java.io.ByteArrayOutputStream; import java.util.Map; -import java.util.function.BiFunction; +import java.util.function.Supplier; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Value; /** - * JavaScript implementation of the {@link ScriptTaskRunner} interface that executes JavaScript - * scripts using GraalVM Polyglot API. + * JavaScript implementation of the {@link ScriptRunner} interface that executes JavaScript scripts + * using GraalVM Polyglot API. */ -public class JavaScriptScriptTaskRunner implements ScriptTaskRunner { +public class JavaScriptScriptTaskRunner implements ScriptRunner { @Override - public RunScriptExecutor.LanguageId identifier() { - return RunScriptExecutor.LanguageId.JS; + public ScriptLanguageId identifier() { + return ScriptLanguageId.JS; } @Override - public BiFunction buildRun( - TaskContext taskContext) { - return (script, input) -> { - String js = identifier().getLang(); - WorkflowApplication application = script.getApplication(); - ByteArrayOutputStream stderr = new ByteArrayOutputStream(); - ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + public WorkflowModel runScript( + ScriptContext script, + WorkflowContext workflowContext, + TaskContext taskContext, + WorkflowModel input) { + WorkflowApplication application = workflowContext.definition().application(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); - WorkflowModelFactory modelFactory = application.modelFactory(); - try (Context ctx = - Context.newBuilder() - .err(stderr) - .out(stdout) - .useSystemExit(true) - .allowCreateProcess(false) - .option("engine.WarnInterpreterOnly", "false") - .build()) { + try (Context ctx = + Context.newBuilder() + .err(stderr) + .out(stdout) + .useSystemExit(true) + .allowCreateProcess(false) + .option("engine.WarnInterpreterOnly", "false") + .build()) { - script - .getArgs() - .forEach( - (key, val) -> { - ctx.getBindings(js).putMember(key, val); - }); + script + .args() + .forEach( + (key, val) -> { + ctx.getBindings(identifier().getLang()).putMember(key, val); + }); - configureProcessEnv(ctx, script.getEnvs()); + configureProcessEnv(ctx, script.envs()); - if (!script.isAwait()) { - script - .getApplication() - .executorService() - .submit( - () -> { - ctx.eval(js, script.getCode()); - }); - return application.modelFactory().fromAny(input); - } + if (!script.isAwait()) { + application + .executorService() + .submit( + () -> { + ctx.eval(identifier().getLang(), script.code()); + }); + return application.modelFactory().fromAny(input); + } - ctx.eval(Source.create(js, script.getCode())); + ctx.eval(Source.create(identifier().getLang(), script.code())); - return switch (script.getReturnType()) { - case ALL -> - modelFactory.fromAny(new ProcessResult(0, stdout.toString(), stderr.toString())); - case NONE -> modelFactory.fromNull(); - case CODE -> modelFactory.from(0); - case STDOUT -> modelFactory.from(stdout.toString().trim()); - case STDERR -> modelFactory.from(stderr.toString().trim()); - }; - } catch (PolyglotException e) { - if (e.getExitStatus() != 0 || e.isSyntaxError()) { - throw new WorkflowException(WorkflowError.runtime(taskContext, e).build()); - } else { - return switch (script.getReturnType()) { - case ALL -> - modelFactory.fromAny( - new ProcessResult( - e.getExitStatus(), stdout.toString().trim(), buildStderr(e, stderr))); - case NONE -> modelFactory.fromNull(); - case CODE -> modelFactory.from(e.getExitStatus()); - case STDOUT -> modelFactory.from(stdout.toString().trim()); - case STDERR -> modelFactory.from(buildStderr(e, stderr)); - }; - } + return modelFromOutput( + script.returnType(), application.modelFactory(), stdout, () -> stderr.toString()); + } catch (PolyglotException e) { + if (e.getExitStatus() != 0 || e.isSyntaxError()) { + throw new WorkflowException(WorkflowError.runtime(taskContext, e).build()); + } else { + return modelFromOutput( + script.returnType(), application.modelFactory(), stdout, () -> buildStderr(e, stderr)); } + } + } + + private WorkflowModel modelFromOutput( + RunTaskConfiguration.ProcessReturnType returnType, + WorkflowModelFactory modelFactory, + ByteArrayOutputStream stdout, + Supplier stderr) { + return switch (returnType) { + case ALL -> + modelFactory.fromAny(new ProcessResult(0, stdout.toString().trim(), stderr.get().trim())); + case NONE -> modelFactory.fromNull(); + case CODE -> modelFactory.from(0); + case STDOUT -> modelFactory.from(stdout.toString().trim()); + case STDERR -> modelFactory.from(stderr.get().trim()); }; } - /** + /* * Gets the stderr message from the PolyglotException or the stderr stream. * * @param e the {@link PolyglotException} thrown during script execution @@ -123,15 +125,15 @@ private String buildStderr(PolyglotException e, ByteArrayOutputStream stderr) { return err.isBlank() ? e.getMessage() : err.trim(); } - /** + /* * Configures the process.env object in the JavaScript context with the provided environment * variables. * * @param context the GraalVM context * @param envs the environment variables to set */ - private void configureProcessEnv(Context context, Map envs) { - String js = RunScriptExecutor.LanguageId.JS.getLang(); + private void configureProcessEnv(Context context, Map envs) { + String js = ScriptLanguageId.JS.getLang(); Value bindings = context.getBindings(js); Value process = context.eval(js, "({ env: {} })"); diff --git a/impl/script-js/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.ScriptTaskRunner b/impl/script-js/src/main/resources/META-INF/services/io.serverlessworkflow.impl.scripts.ScriptRunner similarity index 100% rename from impl/script-js/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.ScriptTaskRunner rename to impl/script-js/src/main/resources/META-INF/services/io.serverlessworkflow.impl.scripts.ScriptRunner