From c77d5359e84bab9960c918a1ba2f382d7b657668 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Nov 2024 14:28:58 +0000 Subject: [PATCH 01/22] [maven-release-plugin] prepare for next development iteration --- api/pom.xml | 2 +- custom-generator/pom.xml | 2 +- impl/pom.xml | 4 ++-- pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 0b0a95d5..d5128d57 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-SNAPSHOT serverlessworkflow-api diff --git a/custom-generator/pom.xml b/custom-generator/pom.xml index 5cbea0a3..7aaedbda 100644 --- a/custom-generator/pom.xml +++ b/custom-generator/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-SNAPSHOT custom-generator diff --git a/impl/pom.xml b/impl/pom.xml index 4c21cc18..3907fb71 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-SNAPSHOT serverlessworkflow-impl @@ -14,7 +14,7 @@ io.serverlessworkflow serverlessworkflow-api - 7.0.0-alpha5 + 7.0.0-SNAPSHOT org.glassfish.jersey.core diff --git a/pom.xml b/pom.xml index e0108a57..4f74b387 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-SNAPSHOT pom Serverless Workflow :: Parent @@ -33,7 +33,7 @@ scm:git:git@github.com:serverlessworkflow/sdk-java.git scm:git:git@github.com:serverlessworkflow/sdk-java.git https://github.com/serverlessworkflow/sdk-java - 7.0.0-alpha5 + HEAD From 9ae8498d33ca86b8bdb8fd0f7c505eec6e521d0e Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Thu, 7 Nov 2024 16:48:32 +0100 Subject: [PATCH 02/22] [Fix #460] Implementing input, output and context Signed-off-by: Francisco Javier Tirado Sarti --- ...tils.java => DefaultWorkflowPosition.java} | 42 ++- .../serverlessworkflow/impl/TaskContext.java | 68 +++++ .../impl/WorkflowContext.java | 70 +++++ .../impl/WorkflowDefinition.java | 43 ++- .../impl/WorkflowExecutionListener.java | 5 +- ...{Expression.java => WorkflowPosition.java} | 11 +- .../impl/executors/AbstractTaskExecutor.java | 117 ++++++++ .../DefaultTaskExecutorFactory.java | 5 +- .../impl/{ => executors}/HttpExecutor.java | 101 ++++--- .../impl/{ => executors}/TaskExecutor.java | 8 +- .../{ => executors}/TaskExecutorFactory.java | 2 +- .../Expression.java} | 27 +- .../{ => expressions}/ExpressionFactory.java | 8 +- .../impl/expressions/ExpressionUtils.java | 78 +++++ .../ExpressionValidationException.java | 14 + .../{jq => expressions}/JQExpression.java | 15 +- .../JQExpressionFactory.java | 5 +- .../impl/expressions/ProxyMap.java | 278 ++++++++++++++++++ .../impl/{ => json}/JsonUtils.java | 8 +- .../impl/{ => json}/MergeUtils.java | 2 +- .../impl/WorkflowDefinitionTest.java | 13 +- .../resources/call-http-query-parameters.yaml | 23 ++ .../{callHttp.yaml => callGetHttp.yaml} | 0 impl/src/test/resources/callPostHttp.yaml | 28 ++ 24 files changed, 847 insertions(+), 124 deletions(-) rename impl/src/main/java/io/serverlessworkflow/impl/{ExpressionUtils.java => DefaultWorkflowPosition.java} (50%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java rename impl/src/main/java/io/serverlessworkflow/impl/{Expression.java => WorkflowPosition.java} (79%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/DefaultTaskExecutorFactory.java (90%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/HttpExecutor.java (51%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/TaskExecutor.java (74%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/TaskExecutorFactory.java (94%) rename impl/src/main/java/io/serverlessworkflow/impl/{AbstractTaskExecutor.java => expressions/Expression.java} (57%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => expressions}/ExpressionFactory.java (83%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java rename impl/src/main/java/io/serverlessworkflow/impl/{jq => expressions}/JQExpression.java (94%) rename impl/src/main/java/io/serverlessworkflow/impl/{jq => expressions}/JQExpressionFactory.java (90%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java rename impl/src/main/java/io/serverlessworkflow/impl/{ => json}/JsonUtils.java (97%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => json}/MergeUtils.java (98%) create mode 100644 impl/src/test/resources/call-http-query-parameters.yaml rename impl/src/test/resources/{callHttp.yaml => callGetHttp.yaml} (100%) create mode 100644 impl/src/test/resources/callPostHttp.yaml diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java similarity index 50% rename from impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java rename to impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java index 45000931..2e51f6a6 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java @@ -15,26 +15,38 @@ */ package io.serverlessworkflow.impl; -public class ExpressionUtils { +public class DefaultWorkflowPosition implements WorkflowPosition { - private static final String EXPR_PREFIX = "${"; - private static final String EXPR_SUFFIX = "}"; + private StringBuilder sb = new StringBuilder(""); - private ExpressionUtils() {} + @Override + public WorkflowPosition addIndex(int index) { + sb.append('/').append(index); + return this; + } - public static String trimExpr(String expr) { - expr = expr.trim(); - if (expr.startsWith(EXPR_PREFIX)) { - expr = trimExpr(expr, EXPR_PREFIX, EXPR_SUFFIX); - } - return expr.trim(); + @Override + public WorkflowPosition addProperty(String prop) { + sb.append('/').append(prop); + return this; + } + + @Override + public String jsonPointer() { + return sb.toString(); + } + + @Override + public String toString() { + return "DefaultWorkflowPosition [sb=" + sb + "]"; } - private static String trimExpr(String expr, String prefix, String suffix) { - expr = expr.substring(prefix.length()); - if (expr.endsWith(suffix)) { - expr = expr.substring(0, expr.length() - suffix.length()); + @Override + public WorkflowPosition back() { + int indexOf = sb.lastIndexOf("/"); + if (indexOf != -1) { + sb.substring(0, indexOf); } - return expr; + return this; } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java new file mode 100644 index 00000000..c9e28f12 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -0,0 +1,68 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; + +public class TaskContext { + + private final JsonNode rawInput; + private final T task; + + private JsonNode input; + private JsonNode output; + private JsonNode rawOutput; + + public TaskContext(JsonNode rawInput, T task) { + this.rawInput = rawInput; + this.input = rawInput; + this.task = task; + } + + public void input(JsonNode input) { + this.input = input; + } + + public JsonNode input() { + return input; + } + + public JsonNode rawInput() { + return rawInput; + } + + public T task() { + return task; + } + + public void rawOutput(JsonNode output) { + this.rawOutput = output; + this.output = output; + } + + public void output(JsonNode output) { + this.output = output; + } + + public JsonNode output() { + return output; + } + + public JsonNode rawOutput() { + return rawOutput; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java new file mode 100644 index 00000000..6982cfd6 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java @@ -0,0 +1,70 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.json.JsonUtils; + +public class WorkflowContext { + + private final WorkflowPosition position; + private JsonNode context; + private final JsonNode input; + + private WorkflowContext(WorkflowPosition position, JsonNode input) { + this.position = position; + this.input = input; + this.context = JsonUtils.mapper().createObjectNode(); + } + + public static Builder builder(JsonNode input) { + return new Builder(input); + } + + public static class Builder { + private WorkflowPosition position = new DefaultWorkflowPosition(); + private JsonNode input; + + private Builder(JsonNode input) { + this.input = input; + } + + public Builder position(WorkflowPosition position) { + this.position = position; + return this; + } + + public WorkflowContext build() { + return new WorkflowContext(position, input); + } + } + + public WorkflowPosition position() { + return position; + } + + public JsonNode context() { + return context; + } + + public void context(JsonNode context) { + this.context = context; + } + + public JsonNode rawInput() { + return input; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index f926a755..ec39c90b 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -15,13 +15,16 @@ */ package io.serverlessworkflow.impl; -import static io.serverlessworkflow.impl.JsonUtils.*; +import static io.serverlessworkflow.impl.json.JsonUtils.*; -import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; +import io.serverlessworkflow.impl.executors.TaskExecutor; +import io.serverlessworkflow.impl.executors.TaskExecutorFactory; +import io.serverlessworkflow.impl.json.JsonUtils; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -43,7 +46,7 @@ private WorkflowDefinition( private final Workflow workflow; private final Collection listeners; private final TaskExecutorFactory taskFactory; - private final Map> taskExecutors = + private final Map> taskExecutors = new ConcurrentHashMap<>(); public static class Builder { @@ -94,40 +97,32 @@ enum State { public class WorkflowInstance { - private final JsonNode input; private JsonNode output; private State state; - - private JsonPointer currentPos; + private WorkflowContext context; private WorkflowInstance(TaskExecutorFactory factory, JsonNode input) { - this.input = input; - this.output = object(); + this.output = input; this.state = State.STARTED; - this.currentPos = JsonPointer.compile("/"); + this.context = WorkflowContext.builder(input).build(); processDo(workflow.getDo()); } private void processDo(List tasks) { - currentPos = currentPos.appendProperty("do"); + context.position().addProperty("do"); int index = 0; for (TaskItem task : tasks) { - currentPos = currentPos.appendIndex(index).appendProperty(task.getName()); - listeners.forEach(l -> l.onTaskStarted(currentPos, task.getTask())); + context.position().addIndex(++index).addProperty(task.getName()); + listeners.forEach(l -> l.onTaskStarted(context.position(), task.getTask())); this.output = - MergeUtils.merge( - taskExecutors - .computeIfAbsent(currentPos, k -> taskFactory.getTaskExecutor(task.getTask())) - .apply(input), - output); - listeners.forEach(l -> l.onTaskEnded(currentPos, task.getTask())); - currentPos = currentPos.head().head(); + taskExecutors + .computeIfAbsent( + context.position().jsonPointer(), + k -> taskFactory.getTaskExecutor(task.getTask())) + .apply(context, output); + listeners.forEach(l -> l.onTaskEnded(context.position(), task.getTask())); + context.position().back().back(); } - currentPos = currentPos.head(); - } - - public String currentPos() { - return currentPos.toString(); } public State state() { diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java index 700c6aa9..ce72c70e 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java @@ -15,12 +15,11 @@ */ package io.serverlessworkflow.impl; -import com.fasterxml.jackson.core.JsonPointer; import io.serverlessworkflow.api.types.Task; public interface WorkflowExecutionListener { - void onTaskStarted(JsonPointer currentPos, Task task); + void onTaskStarted(WorkflowPosition currentPos, Task task); - void onTaskEnded(JsonPointer currentPos, Task task); + void onTaskEnded(WorkflowPosition currentPos, Task task); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/Expression.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java similarity index 79% rename from impl/src/main/java/io/serverlessworkflow/impl/Expression.java rename to impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java index b5bbfc0b..c43d4b2f 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/Expression.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java @@ -15,8 +15,13 @@ */ package io.serverlessworkflow.impl; -import com.fasterxml.jackson.databind.JsonNode; +public interface WorkflowPosition { -public interface Expression { - JsonNode eval(JsonNode input); + String jsonPointer(); + + WorkflowPosition addProperty(String prop); + + WorkflowPosition addIndex(int index); + + WorkflowPosition back(); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java new file mode 100644 index 00000000..36dbbf4f --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -0,0 +1,117 @@ +/* + * 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.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.Export; +import io.serverlessworkflow.api.types.Input; +import io.serverlessworkflow.api.types.Output; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.Expression; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.util.Map; +import java.util.Optional; + +public abstract class AbstractTaskExecutor implements TaskExecutor { + + protected final T task; + protected final ExpressionFactory exprFactory; + + private interface TaskFilter { + JsonNode apply(WorkflowContext workflow, TaskContext task, JsonNode node); + } + + private final Optional> inputProcessor; + private final Optional> outputProcessor; + private final Optional> contextProcessor; + + protected AbstractTaskExecutor(T task, ExpressionFactory exprFactory) { + this.task = task; + this.exprFactory = exprFactory; + this.inputProcessor = Optional.ofNullable(getInputProcessor()); + this.outputProcessor = Optional.ofNullable(getOutputProcessor()); + this.contextProcessor = Optional.ofNullable(getContextProcessor()); + } + + private TaskFilter getInputProcessor() { + if (task.getInput() != null) { + Input input = task.getInput(); + // TODO add schema validator + if (input.getFrom() != null) { + return getTaskFilter(input.getFrom().getString(), input.getFrom().getObject()); + } + } + return null; + } + + private TaskFilter getOutputProcessor() { + if (task.getOutput() != null) { + Output output = task.getOutput(); + // TODO add schema validator + if (output.getAs() != null) { + return getTaskFilter(output.getAs().getString(), output.getAs().getObject()); + } + } + return null; + } + + private TaskFilter getContextProcessor() { + if (task.getExport() != null) { + Export export = task.getExport(); + // TODO add schema validator + if (export.getAs() != null) { + return getTaskFilter(export.getAs().getString(), export.getAs().getObject()); + } + } + return null; + } + + private TaskFilter getTaskFilter(String str, Object object) { + if (str != null) { + Expression expression = exprFactory.getExpression(str); + return expression::eval; + } else { + Object exprObj = ExpressionUtils.buildExpressionObject(object, exprFactory); + return exprObj instanceof Map + ? (w, t, n) -> + JsonUtils.fromValue( + ExpressionUtils.evaluateExpressionMap((Map) exprObj, w, t, n)) + : (w, t, n) -> JsonUtils.fromValue(object); + } + } + + @Override + public JsonNode apply(WorkflowContext workflowContext, JsonNode rawInput) { + TaskContext taskContext = new TaskContext<>(rawInput, task); + inputProcessor.ifPresent( + p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); + taskContext.rawOutput(internalExecute(workflowContext, taskContext, taskContext.input())); + outputProcessor.ifPresent( + p -> taskContext.output(p.apply(workflowContext, taskContext, taskContext.rawOutput()))); + contextProcessor.ifPresent( + p -> + workflowContext.context( + p.apply(workflowContext, taskContext, workflowContext.context()))); + return taskContext.output(); + } + + protected abstract JsonNode internalExecute( + WorkflowContext workflow, TaskContext task, JsonNode node); +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java similarity index 90% rename from impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index fab07d8c..cf49657e 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.impl.jq.JQExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.JQExpressionFactory; public class DefaultTaskExecutorFactory implements TaskExecutorFactory { diff --git a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java similarity index 51% rename from impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index e2c2c42f..60da619c 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -22,8 +22,12 @@ import io.serverlessworkflow.api.types.EndpointUri; import io.serverlessworkflow.api.types.HTTPArguments; import io.serverlessworkflow.api.types.UriTemplate; -import io.serverlessworkflow.api.types.WithHTTPHeaders; -import io.serverlessworkflow.api.types.WithHTTPQuery; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.Expression; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; @@ -33,44 +37,71 @@ import java.net.URI; import java.util.Map; import java.util.Map.Entry; -import java.util.function.Function; public class HttpExecutor extends AbstractTaskExecutor { private static final Client client = ClientBuilder.newClient(); - private final Function targetSupplier; + private final TargetSupplier targetSupplier; + private final Map headersMap; + private final Map queryMap; + private final RequestSupplier requestFunction; - public HttpExecutor(CallHTTP task, ExpressionFactory factory) { - super(task, factory); - this.targetSupplier = getTargetSupplier(task.getWith().getEndpoint()); + @FunctionalInterface + private interface TargetSupplier { + WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node); } - @Override - protected JsonNode internalExecute(JsonNode node) { + @FunctionalInterface + private interface RequestSupplier { + JsonNode apply(Builder request, WorkflowContext workflow, TaskContext task, JsonNode node); + } + + public HttpExecutor(CallHTTP task, ExpressionFactory factory) { + super(task, factory); HTTPArguments httpArgs = task.getWith(); - WithHTTPQuery query = httpArgs.getQuery(); - WebTarget target = targetSupplier.apply(node); - if (query != null) { - for (Entry entry : query.getAdditionalProperties().entrySet()) { - target = target.queryParam(entry.getKey(), entry.getValue()); - } - } - Builder request = target.request(); - WithHTTPHeaders headers = httpArgs.getHeaders(); - if (headers != null) { - headers.getAdditionalProperties().forEach(request::header); - } + this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint()); + this.headersMap = + httpArgs.getHeaders() != null + ? ExpressionUtils.buildExpressionMap( + httpArgs.getHeaders().getAdditionalProperties(), factory) + : Map.of(); + this.queryMap = + httpArgs.getQuery() != null + ? ExpressionUtils.buildExpressionMap( + httpArgs.getQuery().getAdditionalProperties(), factory) + : Map.of(); switch (httpArgs.getMethod().toUpperCase()) { + case HttpMethod.POST: + Object body = ExpressionUtils.buildExpressionObject(httpArgs.getBody(), factory); + this.requestFunction = + (request, workflow, context, node) -> + request.post( + Entity.json( + ExpressionUtils.evaluateExpressionObject(body, workflow, context, node)), + JsonNode.class); + break; case HttpMethod.GET: default: - return request.get(JsonNode.class); - case HttpMethod.POST: - return request.post(Entity.json(httpArgs.getBody()), JsonNode.class); + this.requestFunction = (request, w, t, n) -> request.get(JsonNode.class); } } - private Function getTargetSupplier(Endpoint endpoint) { + @Override + protected JsonNode internalExecute( + WorkflowContext workflow, TaskContext taskContext, JsonNode input) { + WebTarget target = targetSupplier.apply(workflow, taskContext, input); + for (Entry entry : + ExpressionUtils.evaluateExpressionMap(queryMap, workflow, taskContext, input).entrySet()) { + target = target.queryParam(entry.getKey(), entry.getValue()); + } + Builder request = target.request(); + ExpressionUtils.evaluateExpressionMap(headersMap, workflow, taskContext, input) + .forEach(request::header); + return requestFunction.apply(request, workflow, taskContext, input); + } + + private TargetSupplier getTargetSupplier(Endpoint endpoint) { if (endpoint.getEndpointConfiguration() != null) { EndpointUri uri = endpoint.getEndpointConfiguration().getUri(); if (uri.getLiteralEndpointURI() != null) { @@ -86,7 +117,7 @@ private Function getTargetSupplier(Endpoint endpoint) { throw new IllegalArgumentException("Invalid endpoint definition " + endpoint); } - private Function getURISupplier(UriTemplate template) { + private TargetSupplier getURISupplier(UriTemplate template) { if (template.getLiteralUri() != null) { return new URISupplier(template.getLiteralUri()); } else if (template.getLiteralUriTemplate() != null) { @@ -95,7 +126,7 @@ private Function getURISupplier(UriTemplate template) { throw new IllegalArgumentException("Invalid uritemplate definition " + template); } - private class URISupplier implements Function { + private class URISupplier implements TargetSupplier { private final URI uri; public URISupplier(URI uri) { @@ -103,12 +134,12 @@ public URISupplier(URI uri) { } @Override - public WebTarget apply(JsonNode input) { + public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { return client.target(uri); } } - private class URITemplateSupplier implements Function { + private class URITemplateSupplier implements TargetSupplier { private final String uri; public URITemplateSupplier(String uri) { @@ -116,15 +147,15 @@ public URITemplateSupplier(String uri) { } @Override - public WebTarget apply(JsonNode input) { + public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { return client .target(uri) .resolveTemplates( - JsonUtils.mapper().convertValue(input, new TypeReference>() {})); + JsonUtils.mapper().convertValue(node, new TypeReference>() {})); } } - private class ExpressionURISupplier implements Function { + private class ExpressionURISupplier implements TargetSupplier { private Expression expr; public ExpressionURISupplier(String expr) { @@ -132,8 +163,8 @@ public ExpressionURISupplier(String expr) { } @Override - public WebTarget apply(JsonNode input) { - return client.target(expr.eval(input).asText()); + public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { + return client.target(expr.eval(workflow, task, node).asText()); } } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java similarity index 74% rename from impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java index 83c4bd18..8c896385 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; -import java.util.function.UnaryOperator; +import io.serverlessworkflow.impl.WorkflowContext; +import java.util.function.BiFunction; -public interface TaskExecutor extends UnaryOperator {} +public interface TaskExecutor + extends BiFunction {} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java similarity index 94% rename from impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java index 69eaa0a0..3a9068c3 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java similarity index 57% rename from impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java index 13181603..37206712 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java @@ -13,29 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.expressions; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; -public abstract class AbstractTaskExecutor implements TaskExecutor { - - protected final T task; - protected final ExpressionFactory exprFactory; - - protected AbstractTaskExecutor(T task, ExpressionFactory exprFactory) { - this.task = task; - this.exprFactory = exprFactory; - } - - @Override - public JsonNode apply(JsonNode node) { - - // do input filtering - return internalExecute(node); - // do output filtering - - } - - protected abstract JsonNode internalExecute(JsonNode node); +public interface Expression { + JsonNode eval( + WorkflowContext workflowContext, TaskContext context, JsonNode node); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java similarity index 83% rename from impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java index 8f9c1dd1..4d07d5af 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java @@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.expressions; public interface ExpressionFactory { - + /** + * @throws ExpressionValidationException + * @param expression + * @return + */ Expression getExpression(String expression); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java new file mode 100644 index 00000000..7f776322 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java @@ -0,0 +1,78 @@ +/* + * 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.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.util.Map; + +public class ExpressionUtils { + + private static final String EXPR_PREFIX = "${"; + private static final String EXPR_SUFFIX = "}"; + + private ExpressionUtils() {} + + public static Map buildExpressionMap( + Map origMap, ExpressionFactory factory) { + return new ProxyMap(origMap, o -> isExpr(o) ? factory.getExpression(o.toString()) : o); + } + + public static Map evaluateExpressionMap( + Map origMap, WorkflowContext workflow, TaskContext task, JsonNode n) { + return new ProxyMap( + origMap, + o -> + o instanceof Expression + ? JsonUtils.toJavaValue(((Expression) o).eval(workflow, task, n)) + : o); + } + + public static Object buildExpressionObject(Object obj, ExpressionFactory factory) { + return obj instanceof Map + ? ExpressionUtils.buildExpressionMap((Map) obj, factory) + : obj; + } + + public static Object evaluateExpressionObject( + Object obj, WorkflowContext workflow, TaskContext task, JsonNode node) { + return obj instanceof Map + ? ExpressionUtils.evaluateExpressionMap((Map) obj, workflow, task, node) + : obj; + } + + public static boolean isExpr(Object expr) { + return expr instanceof String && ((String) expr).startsWith(EXPR_PREFIX); + } + + public static String trimExpr(String expr) { + expr = expr.trim(); + if (expr.startsWith(EXPR_PREFIX)) { + expr = trimExpr(expr, EXPR_PREFIX, EXPR_SUFFIX); + } + return expr.trim(); + } + + private static String trimExpr(String expr, String prefix, String suffix) { + expr = expr.substring(prefix.length()); + if (expr.endsWith(suffix)) { + expr = expr.substring(0, expr.length() - suffix.length()); + } + return expr; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java new file mode 100644 index 00000000..16fe144f --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java @@ -0,0 +1,14 @@ +package io.serverlessworkflow.impl.expressions; + +public class ExpressionValidationException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExpressionValidationException(String message) { + super(message); + } + + public ExpressionValidationException(String message, Throwable ex) { + super(message, ex); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java similarity index 94% rename from impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java index b77f34a2..2e64e17a 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl.jq; +package io.serverlessworkflow.impl.expressions; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import io.serverlessworkflow.impl.Expression; -import io.serverlessworkflow.impl.JsonUtils; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.json.JsonUtils; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; @@ -176,14 +178,15 @@ public JsonNode getResult() { } @Override - public JsonNode eval(JsonNode context) { + public JsonNode eval( + WorkflowContext workflow, TaskContext task, JsonNode node) { TypedOutput output = output(JsonNode.class); try { - internalExpr.apply(this.scope.get(), context, output); + internalExpr.apply(this.scope.get(), node, output); return output.getResult(); } catch (JsonQueryException e) { throw new IllegalArgumentException( - "Unable to evaluate content " + context + " using expr " + expr, e); + "Unable to evaluate content " + node + " using expr " + expr, e); } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java similarity index 90% rename from impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java index 787842d6..0375224a 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl.jq; +package io.serverlessworkflow.impl.expressions; -import io.serverlessworkflow.impl.Expression; -import io.serverlessworkflow.impl.ExpressionFactory; -import io.serverlessworkflow.impl.ExpressionUtils; import java.util.function.Supplier; import net.thisptr.jackson.jq.BuiltinFunctionLoader; import net.thisptr.jackson.jq.Scope; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java new file mode 100644 index 00000000..bf4464b2 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java @@ -0,0 +1,278 @@ +/* + * 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.expressions; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; + +public class ProxyMap implements Map { + + private final Map map; + private final UnaryOperator function; + + public ProxyMap(Map map, UnaryOperator function) { + this.map = map; + this.function = function; + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public Object get(Object key) { + return processValue(map.get(key)); + } + + @Override + public Object put(String key, Object value) { + return map.put(key, processValue(value)); + } + + @Override + public Object remove(Object key) { + return map.remove(key); + } + + @Override + public void putAll(Map m) { + map.putAll(m); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public Collection values() { + return new ProxyCollection(map.values()); + } + + @Override + public Set> entrySet() { + return new ProxyEntrySet(map.entrySet()); + } + + private abstract class AbstractProxyCollection { + + protected Collection values; + + protected AbstractProxyCollection(Collection values) { + this.values = values; + } + + public int size() { + return values.size(); + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public boolean contains(Object o) { + return values.contains(o); + } + + public boolean remove(Object o) { + return values.remove(o); + } + + public boolean containsAll(Collection c) { + return values.containsAll(c); + } + + public boolean retainAll(Collection c) { + return values.retainAll(c); + } + + public boolean removeAll(Collection c) { + return values.removeAll(c); + } + + public void clear() { + values.clear(); + } + + public boolean addAll(Collection c) { + return values.addAll(c); + } + + public boolean add(T e) { + return values.add(e); + } + } + + private class ProxyEntrySet extends AbstractProxyCollection> + implements Set> { + + public ProxyEntrySet(Set> entrySet) { + super(entrySet); + } + + @Override + public Iterator> iterator() { + return new ProxyEntryIterator(values.iterator()); + } + + @Override + public Object[] toArray() { + return processEntries(values.toArray()); + } + + @Override + public T[] toArray(T[] a) { + return processEntries(values.toArray(a)); + } + + private T[] processEntries(T[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = (T) new ProxyEntry((Entry) array[i]); + } + return array; + } + } + + private class ProxyCollection extends AbstractProxyCollection + implements Collection { + + public ProxyCollection(Collection values) { + super(values); + } + + @Override + public Iterator iterator() { + return new ProxyIterator(values.iterator()); + } + + @Override + public Object[] toArray() { + return processArray(values.toArray()); + } + + @Override + public T[] toArray(T[] a) { + return processArray(values.toArray(a)); + } + + private S[] processArray(S[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = (S) processValue(array[i]); + } + return array; + } + } + + private class ProxyEntry implements Entry { + + private Entry entry; + + private ProxyEntry(Entry entry) { + this.entry = entry; + } + + @Override + public String getKey() { + return entry.getKey(); + } + + @Override + public Object getValue() { + return processValue(entry.getValue()); + } + + @Override + public Object setValue(Object value) { + return entry.setValue(value); + } + } + + private class ProxyIterator implements Iterator { + + private Iterator iter; + + public ProxyIterator(Iterator iter) { + this.iter = iter; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Object next() { + return processValue(iter.next()); + } + + @Override + public void remove() { + iter.remove(); + } + } + + private class ProxyEntryIterator implements Iterator> { + + private Iterator> iter; + + public ProxyEntryIterator(Iterator> iter) { + this.iter = iter; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Entry next() { + return new ProxyEntry(iter.next()); + } + + @Override + public void remove() { + iter.remove(); + } + } + + private Object processValue(T obj) { + return function.apply(obj); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java similarity index 97% rename from impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java rename to impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java index b00b14f1..a13c8313 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.json; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -87,6 +87,10 @@ public static JsonNode fromValue(Object value) { } } + public static Object toJavaValue(Object object) { + return object instanceof JsonNode ? toJavaValue((JsonNode) object) : object; + } + public static JsonNode fromString(String value) { String trimmedValue = value.trim(); if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) { @@ -201,7 +205,7 @@ private static ArrayNode mapToArray(Collection collection, ArrayNode arrayNod return arrayNode; } - static ObjectNode object() { + public static ObjectNode object() { return mapper.createObjectNode(); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java similarity index 98% rename from impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java rename to impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java index 8c1ec1de..a3615d35 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.json; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index 66ef5d86..ba842e4e 100644 --- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -46,7 +46,16 @@ private static Stream provideParameters() { new Condition<>( o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"); return Stream.of( - Arguments.of("callHttp.yaml", petInput, petCondition), - Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition)); + Arguments.of("callGetHttp.yaml", petInput, petCondition), + Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition), + Arguments.of( + "call-http-query-parameters.yaml", + Map.of("searchQuery", "R2-D2"), + new Condition<>( + o -> ((Map) o).get("count").equals(1), "R2D2Condition")), + Arguments.of( + "callPostHttp.yaml", + Map.of("name", "Javierito", "status", "available"), + new Condition<>(o -> o.equals("Javierito"), "CallHttpPostCondition"))); } } diff --git a/impl/src/test/resources/call-http-query-parameters.yaml b/impl/src/test/resources/call-http-query-parameters.yaml new file mode 100644 index 00000000..75f33378 --- /dev/null +++ b/impl/src/test/resources/call-http-query-parameters.yaml @@ -0,0 +1,23 @@ +document: + dsl: 1.0.0-alpha2 + namespace: examples + name: http-query-params + version: 1.0.0-alpha2 +input: + schema: + document: + type: object + required: + - searchQuery + properties: + searchQuery: + type: string +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + diff --git a/impl/src/test/resources/callHttp.yaml b/impl/src/test/resources/callGetHttp.yaml similarity index 100% rename from impl/src/test/resources/callHttp.yaml rename to impl/src/test/resources/callGetHttp.yaml diff --git a/impl/src/test/resources/callPostHttp.yaml b/impl/src/test/resources/callPostHttp.yaml new file mode 100644 index 00000000..d898dbf7 --- /dev/null +++ b/impl/src/test/resources/callPostHttp.yaml @@ -0,0 +1,28 @@ +document: + dsl: 1.0.0-alpha1 + namespace: default + name: http-call-with-response-output + version: 1.0.0 +do: + - postPet: + call: http + with: + method: post + endpoint: + uri: https://petstore.swagger.io/v2/pet + body: + name: ${.name} + status: ${.status} + output: + as: .id + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + input: + from: + petId: ${.} + output: + as: .name \ No newline at end of file From c72cfe5dc1d8f84c5443344094c6eca31daa65b4 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Mon, 11 Nov 2024 18:19:25 +0100 Subject: [PATCH 03/22] [Fix #461] oneOf options must inherit union class common part Signed-off-by: Francisco Javier Tirado Sarti --- .../api/OneOfValueProvider.java | 4 +- .../serverlessworkflow/api/FeaturesTest.java | 3 +- .../features/call-http-query-parameters.yaml | 24 +++ .../generator/AllAnyOneOfSchemaRule.java | 144 ++++++++++-------- .../generator/GeneratorUtils.java | 2 +- 5 files changed, 108 insertions(+), 69 deletions(-) create mode 100644 api/src/test/resources/features/call-http-query-parameters.yaml diff --git a/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java b/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java index f3d2ab26..9d17b872 100644 --- a/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java +++ b/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java @@ -15,6 +15,6 @@ */ package io.serverlessworkflow.api; -public interface OneOfValueProvider { - Object get(); +public interface OneOfValueProvider { + T get(); } diff --git a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java index fd16b952..5672bbc9 100644 --- a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java @@ -45,7 +45,8 @@ public class FeaturesTest { "features/try.yaml", "features/listen.yaml", "features/callFunction.yaml", - "features/callCustomFunction.yaml" + "features/callCustomFunction.yaml", + "features/call-http-query-parameters.yaml" }) public void testSpecFeaturesParsing(String workflowLocation) throws IOException { Workflow workflow = readWorkflowFromClasspath(workflowLocation); diff --git a/api/src/test/resources/features/call-http-query-parameters.yaml b/api/src/test/resources/features/call-http-query-parameters.yaml new file mode 100644 index 00000000..95934315 --- /dev/null +++ b/api/src/test/resources/features/call-http-query-parameters.yaml @@ -0,0 +1,24 @@ +document: + dsl: 1.0.0-alpha2 + namespace: examples + name: http-query-params + version: 1.0.0-alpha2 +input: + schema: + format: json + document: + type: object + required: + - searchQuery + properties: + searchQuery: + type: string +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java index ab0c1a23..8d5b921d 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java @@ -150,82 +150,115 @@ public JType apply( ruleFactory.getEnumRule().apply(nodeName, schemaNode, parent, generatableType, schema); } else if (!schemaNode.has("properties") && unionTypes.isEmpty() && refType.isPresent()) { javaType = refType.get(); + } else if (!unionTypes.isEmpty()) { + JPackage container = generatableType.getPackage(); + JType generatedType = + ruleFactory.getTypeRule().apply(nodeName, schemaNode, parent, container, schema); + try { + JDefinedClass unionClass; + Optional commonType; + if (generatedType instanceof JDefinedClass) { + JDefinedClass clazz = (JDefinedClass) generatedType; + if (clazz.methods().isEmpty()) { + unionClass = clazz; + commonType = Optional.empty(); + } else { + unionClass = container._class(clazz.name() + "Union"); + commonType = Optional.of(clazz); + } + } else { + unionClass = + container._class( + ruleFactory.getNameHelper().getUniqueClassName(nodeName, schemaNode, container)); + commonType = Optional.empty(); + } + javaType = + populateRef(populateClass(schema, unionClass, commonType, unionTypes), refType, schema); + schema.setJavaTypeIfEmpty(javaType); + } catch (JClassAlreadyExistsException ex) { + throw new IllegalStateException(ex); + } } else { javaType = ruleFactory .getTypeRule() .apply(nodeName, schemaNode, parent, generatableType.getPackage(), schema); if (javaType instanceof JDefinedClass) { - populateClass(schema, (JDefinedClass) javaType, refType, unionTypes); - } else if (!unionTypes.isEmpty()) { - javaType = - createUnionClass( - schema, nodeName, schemaNode, generatableType.getPackage(), refType, unionTypes); + populateRef((JDefinedClass) javaType, refType, schema); } schema.setJavaTypeIfEmpty(javaType); } + return javaType; } private JDefinedClass populateClass( Schema parentSchema, JDefinedClass definedClass, - Optional refType, + Optional commonType, Collection unionTypes) { - JType clazzClass = definedClass.owner()._ref(Object.class); + JFieldVar valueField = + definedClass.field( + JMod.PRIVATE, + commonType.orElse(definedClass.owner().ref(Object.class)), + ruleFactory.getNameHelper().getPropertyName("value", null), + null); - Optional valueField; - if (!unionTypes.isEmpty()) { - valueField = - Optional.of( - definedClass.field( - JMod.PRIVATE, - clazzClass, - ruleFactory.getNameHelper().getPropertyName("value", null), - null)); + definedClass._implements( + definedClass + .owner() + .ref(GeneratorUtils.ONE_OF_VALUE_PROVIDER_INTERFACE_NAME) + .narrow(valueField.type())); - definedClass._implements( - definedClass.owner().ref(GeneratorUtils.ONE_OF_VALUE_PROVIDER_INTERFACE_NAME)); + GeneratorUtils.implementInterface(definedClass, valueField); - GeneratorUtils.implementInterface(definedClass, valueField.orElseThrow()); + try { + JDefinedClass serializer = generateSerializer(definedClass); + definedClass.annotate(JsonSerialize.class).param("using", serializer); + } catch (JClassAlreadyExistsException ex) { + // already serialized aware + } - try { - JDefinedClass serializer = generateSerializer(definedClass); - definedClass.annotate(JsonSerialize.class).param("using", serializer); - } catch (JClassAlreadyExistsException ex) { - // already serialized aware - } + try { + JDefinedClass deserializer = generateDeserializer(definedClass, unionTypes); + definedClass.annotate(JsonDeserialize.class).param("using", deserializer); + } catch (JClassAlreadyExistsException ex) { + // already deserialized aware + } - try { - JDefinedClass deserializer = generateDeserializer(definedClass, unionTypes); - definedClass.annotate(JsonDeserialize.class).param("using", deserializer); - } catch (JClassAlreadyExistsException ex) { - // already deserialized aware - } + Collection stringTypes = new ArrayList<>(); + for (JTypeWrapper unionType : unionTypes) { + if (isStringType(unionType.getType())) { + stringTypes.add(unionType); + } else { - Collection stringTypes = new ArrayList<>(); - for (JTypeWrapper unionType : unionTypes) { - if (isStringType(unionType.getType())) { - stringTypes.add(unionType); - } else { - wrapIt(parentSchema, definedClass, valueField, unionType.getType(), unionType.getNode()); + if (unionType.getType() instanceof JDefinedClass) { + commonType.ifPresent( + c -> ((JDefinedClass) unionType.getType())._extends((JDefinedClass) c)); } + wrapIt( + parentSchema, + definedClass, + Optional.of(valueField), + unionType.getType(), + unionType.getNode()); } - if (!stringTypes.isEmpty()) { - wrapStrings(parentSchema, definedClass, valueField, stringTypes); - } - - } else { - valueField = Optional.empty(); } + if (!stringTypes.isEmpty()) { + wrapStrings(parentSchema, definedClass, valueField, stringTypes); + } + return definedClass; + } + private JDefinedClass populateRef( + JDefinedClass definedClass, Optional refType, Schema parentSchema) { refType.ifPresent( type -> { if (type instanceof JClass) { definedClass._extends((JClass) type); } else { - wrapIt(parentSchema, definedClass, valueField, type, null); + wrapIt(parentSchema, definedClass, Optional.empty(), type, null); } }); @@ -280,25 +313,6 @@ private JDefinedClass generateDeserializer( return definedClass; } - private JDefinedClass createUnionClass( - Schema parentSchema, - String nodeName, - JsonNode schemaNode, - JPackage container, - Optional refType, - Collection unionTypes) { - try { - return populateClass( - parentSchema, - container._class( - ruleFactory.getNameHelper().getUniqueClassName(nodeName, schemaNode, container)), - refType, - unionTypes); - } catch (JClassAlreadyExistsException e) { - throw new IllegalArgumentException(e); - } - } - private void wrapIt( Schema parentSchema, JDefinedClass definedClass, @@ -316,7 +330,7 @@ private void wrapIt( private void wrapStrings( Schema parentSchema, JDefinedClass definedClass, - Optional valueField, + JFieldVar valueField, Collection stringTypes) { Iterator iter = stringTypes.iterator(); JTypeWrapper first = iter.next(); @@ -330,7 +344,7 @@ private void wrapStrings( JFieldVar instanceField = getInstanceField(parentSchema, definedClass, first.getType(), first.getNode()); JVar instanceParam = constructor.param(first.type, instanceField.name()); - valueField.ifPresent(v -> body.assign(JExpr._this().ref(v), instanceParam)); + body.assign(JExpr._this().ref(valueField), instanceParam); if (pattern != null) { JConditional condition = body._if(getPatternCondition(pattern, body, instanceField, instanceParam, definedClass)); diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java index ce3badc2..7248d594 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java @@ -60,7 +60,7 @@ public static JDefinedClass deserializerClass(JDefinedClass relatedClass) } public static JMethod implementInterface(JDefinedClass definedClass, JFieldVar valueField) { - JMethod method = definedClass.method(JMod.PUBLIC, Object.class, "get"); + JMethod method = definedClass.method(JMod.PUBLIC, valueField.type(), "get"); method.annotate(Override.class); method.body()._return(valueField); return method; From 4eae5aba7c92f9a30b717bbafd2d86e3b659fcb6 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Mon, 11 Nov 2024 22:37:16 +0100 Subject: [PATCH 04/22] [Fix #461] alternative approach Signed-off-by: Francisco Javier Tirado Sarti --- .../api/WorkflowReader.java | 15 ++ .../api/WorkflowWriter.java | 18 ++ .../serialization/DeserializeHelper.java | 53 ++++- .../serialization/OneOfSetter.java | 28 +++ .../serverlessworkflow/api/FeaturesTest.java | 19 +- .../authentication-bearer-uri-format.yaml | 15 ++ .../features/authentication-bearer.yaml | 15 ++ .../authentication-oauth2-secret.yaml | 18 ++ .../features/authentication-oauth2.yaml | 22 ++ .../features/authentication-oidc-secret.yaml | 18 ++ .../features/authentication-oidc.yaml | 19 ++ .../features/authentication-reusable.yaml | 19 ++ .../generator/AllAnyOneOfSchemaRule.java | 190 +++++++++++------- .../generator/GeneratorUtils.java | 4 +- .../generator/UnevaluatedPropertiesRule.java | 4 +- 15 files changed, 368 insertions(+), 89 deletions(-) create mode 100644 api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java create mode 100644 api/src/test/resources/features/authentication-bearer-uri-format.yaml create mode 100644 api/src/test/resources/features/authentication-bearer.yaml create mode 100644 api/src/test/resources/features/authentication-oauth2-secret.yaml create mode 100644 api/src/test/resources/features/authentication-oauth2.yaml create mode 100644 api/src/test/resources/features/authentication-oidc-secret.yaml create mode 100644 api/src/test/resources/features/authentication-oidc.yaml create mode 100644 api/src/test/resources/features/authentication-reusable.yaml diff --git a/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java b/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java index 01c6c8b0..4decc696 100644 --- a/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java +++ b/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java @@ -16,10 +16,12 @@ package io.serverlessworkflow.api; import io.serverlessworkflow.api.types.Workflow; +import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; @@ -37,6 +39,19 @@ public static Workflow readWorkflow(Path path, WorkflowFormat format) throws IOE return format.mapper().readValue(Files.readAllBytes(path), Workflow.class); } + public static Workflow readWorkflow(byte[] content, WorkflowFormat format) throws IOException { + try (InputStream input = new ByteArrayInputStream(content)) { + return readWorkflow(input, format); + } + } + + public static Workflow readWorkflowFromString(String content, WorkflowFormat format) + throws IOException { + try (Reader reader = new StringReader(content)) { + return readWorkflow(reader, format); + } + } + public static Workflow readWorkflowFromClasspath(String classpath) throws IOException { return readWorkflowFromClasspath( classpath, diff --git a/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java b/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java index f98e6402..29115396 100644 --- a/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java +++ b/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java @@ -16,8 +16,10 @@ package io.serverlessworkflow.api; import io.serverlessworkflow.api.types.Workflow; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.StringWriter; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; @@ -45,5 +47,21 @@ public static void writeWorkflow(Path output, Workflow workflow, WorkflowFormat } } + public static String workflowAsString(Workflow workflow, WorkflowFormat format) + throws IOException { + try (Writer writer = new StringWriter()) { + writeWorkflow(writer, workflow, format); + return writer.toString(); + } + } + + public static byte[] workflowAsBytes(Workflow workflow, WorkflowFormat format) + throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + writeWorkflow(out, workflow, format); + return out.toByteArray(); + } + } + private WorkflowWriter() {} } diff --git a/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java b/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java index 72b4cce0..cfbd54ca 100644 --- a/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java +++ b/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java @@ -21,24 +21,57 @@ import com.fasterxml.jackson.databind.JsonMappingException; import jakarta.validation.ConstraintViolationException; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; public class DeserializeHelper { public static T deserializeOneOf( - JsonParser p, Class targetClass, Collection> unionTypes) throws IOException { + JsonParser p, Class targetClass, Collection> oneOfTypes) throws IOException { TreeNode node = p.readValueAsTree(); - JsonProcessingException ex = - new JsonMappingException(p, "Problem deserializing " + targetClass); - for (Class unionType : unionTypes) { - try { - Object object = p.getCodec().treeToValue(node, unionType); - return targetClass.getConstructor(unionType).newInstance(object); - } catch (IOException | ReflectiveOperationException | ConstraintViolationException io) { - ex.addSuppressed(io); + try { + T result = targetClass.getDeclaredConstructor().newInstance(); + Collection exceptions = new ArrayList<>(); + for (Class oneOfType : oneOfTypes) { + try { + assingIt(p, result, node, targetClass, oneOfType); + break; + } catch (IOException | ConstraintViolationException | InvocationTargetException ex) { + exceptions.add(ex); + } + } + if (exceptions.size() == oneOfTypes.size()) { + JsonMappingException ex = + new JsonMappingException( + p, + String.format( + "Error deserializing class %s, all oneOf alternatives %s has failed ", + targetClass, oneOfTypes)); + exceptions.forEach(ex::addSuppressed); + throw ex; + } + return result; + } catch (ReflectiveOperationException ex) { + throw new IllegalStateException(ex); + } + } + + private static void assingIt( + JsonParser p, T result, TreeNode node, Class targetClass, Class type) + throws JsonProcessingException, ReflectiveOperationException { + findSetMethod(targetClass, type).invoke(result, p.getCodec().treeToValue(node, type)); + } + + private static Method findSetMethod(Class targetClass, Class type) { + for (Method method : targetClass.getMethods()) { + OneOfSetter oneOfSetter = method.getAnnotation(OneOfSetter.class); + if (oneOfSetter != null && type.equals(oneOfSetter.value())) { + return method; } } - throw ex; + throw new IllegalStateException("Cannot find a setter for type " + type); } public static T deserializeItem(JsonParser p, Class targetClass, Class valueClass) diff --git a/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java b/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java new file mode 100644 index 00000000..098df425 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java @@ -0,0 +1,28 @@ +/* + * 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.serialization; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(METHOD) +public @interface OneOfSetter { + Class value(); +} diff --git a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java index 5672bbc9..81d10ecf 100644 --- a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java @@ -17,7 +17,10 @@ import static io.serverlessworkflow.api.WorkflowReader.readWorkflow; import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static io.serverlessworkflow.api.WorkflowWriter.workflowAsBytes; +import static io.serverlessworkflow.api.WorkflowWriter.workflowAsString; import static io.serverlessworkflow.api.WorkflowWriter.writeWorkflow; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import io.serverlessworkflow.api.types.Workflow; @@ -32,6 +35,13 @@ public class FeaturesTest { @ParameterizedTest @ValueSource( strings = { + "features/authentication-bearer.yaml", + "features/authentication-bearer-uri-format.yaml", + "features/authentication-oauth2.yaml", + "features/authentication-oauth2-secret.yaml", + "features/authentication-oidc.yaml", + "features/authentication-oidc-secret.yaml", + "features/authentication-reusable.yaml", "features/callHttp.yaml", "features/callOpenAPI.yaml", "features/composite.yaml", @@ -51,7 +61,7 @@ public class FeaturesTest { public void testSpecFeaturesParsing(String workflowLocation) throws IOException { Workflow workflow = readWorkflowFromClasspath(workflowLocation); assertWorkflow(workflow); - assertWorkflow(writeAndReadInMemory(workflow)); + assertWorkflowEquals(workflow, writeAndReadInMemory(workflow)); } private static Workflow writeAndReadInMemory(Workflow workflow) throws IOException { @@ -70,4 +80,11 @@ private static void assertWorkflow(Workflow workflow) { assertNotNull(workflow.getDocument()); assertNotNull(workflow.getDo()); } + + private static void assertWorkflowEquals(Workflow workflow, Workflow other) throws IOException { + assertThat(workflowAsString(workflow, WorkflowFormat.YAML)) + .isEqualTo(workflowAsString(other, WorkflowFormat.YAML)); + assertThat(workflowAsBytes(workflow, WorkflowFormat.JSON)) + .isEqualTo(workflowAsBytes(other, WorkflowFormat.JSON)); + } } diff --git a/api/src/test/resources/features/authentication-bearer-uri-format.yaml b/api/src/test/resources/features/authentication-bearer-uri-format.yaml new file mode 100644 index 00000000..b0019fbb --- /dev/null +++ b/api/src/test/resources/features/authentication-bearer-uri-format.yaml @@ -0,0 +1,15 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + bearer: + token: ${ .token } diff --git a/api/src/test/resources/features/authentication-bearer.yaml b/api/src/test/resources/features/authentication-bearer.yaml new file mode 100644 index 00000000..f0c42741 --- /dev/null +++ b/api/src/test/resources/features/authentication-bearer.yaml @@ -0,0 +1,15 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: bearer-auth-uri-format + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/1 + authentication: + bearer: + token: ${ .token } \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oauth2-secret.yaml b/api/src/test/resources/features/authentication-oauth2-secret.yaml new file mode 100644 index 00000000..635076ab --- /dev/null +++ b/api/src/test/resources/features/authentication-oauth2-secret.yaml @@ -0,0 +1,18 @@ +document: + dsl: 1.0.0-alpha1 + namespace: examples + name: oauth2-authentication + version: 1.0.0-alpha1 +use: + secrets: + - mySecret +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oauth2: + use: mySecret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oauth2.yaml b/api/src/test/resources/features/authentication-oauth2.yaml new file mode 100644 index 00000000..625a1e2c --- /dev/null +++ b/api/src/test/resources/features/authentication-oauth2.yaml @@ -0,0 +1,22 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: oauth2-authentication + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oauth2: + authority: http://keycloak/realms/fake-authority + endpoints: #optional + token: /auth/token #defaults to /oauth2/token + introspection: /auth/introspect #defaults to /oauth2/introspect + grant: client_credentials + client: + id: workflow-runtime-id + secret: workflow-runtime-secret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oidc-secret.yaml b/api/src/test/resources/features/authentication-oidc-secret.yaml new file mode 100644 index 00000000..19c387c1 --- /dev/null +++ b/api/src/test/resources/features/authentication-oidc-secret.yaml @@ -0,0 +1,18 @@ +document: + dsl: 1.0.0-alpha1 + namespace: examples + name: oidc-authentication + version: 1.0.0-alpha1 +use: + secrets: + - mySecret +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oidc: + use: mySecret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oidc.yaml b/api/src/test/resources/features/authentication-oidc.yaml new file mode 100644 index 00000000..18aec74d --- /dev/null +++ b/api/src/test/resources/features/authentication-oidc.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: oidc-authentication + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oidc: + authority: http://keycloak/realms/fake-authority #endpoints are resolved using the OIDC configuration located at '/.well-known/openid-configuration' + grant: client_credentials + client: + id: workflow-runtime-id + secret: workflow-runtime-secret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-reusable.yaml b/api/src/test/resources/features/authentication-reusable.yaml new file mode 100644 index 00000000..a5da803d --- /dev/null +++ b/api/src/test/resources/features/authentication-reusable.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: bearer-auth + version: '0.1.0' +use: + authentications: + petStoreAuth: + bearer: + token: ${ .token } +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + use: petStoreAuth diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java index 8d5b921d..df3ead34 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java @@ -136,68 +136,74 @@ public JType apply( Schema schema) { Optional refType = refType(nodeName, schemaNode, parent, generatableType, schema); - List unionTypes = new ArrayList<>(); + List oneOfTypes = new ArrayList<>(); + List allOfTypes = new ArrayList<>(); - unionType("oneOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); - unionType("anyOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); - unionType("allOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); + unionType("oneOf", nodeName, schemaNode, parent, generatableType, schema, oneOfTypes); + unionType("anyOf", nodeName, schemaNode, parent, generatableType, schema, oneOfTypes); + unionType("allOf", nodeName, schemaNode, parent, generatableType, schema, allOfTypes); - Collections.sort(unionTypes); + Collections.sort(oneOfTypes); JType javaType; if (schemaNode.has("enum")) { javaType = ruleFactory.getEnumRule().apply(nodeName, schemaNode, parent, generatableType, schema); - } else if (!schemaNode.has("properties") && unionTypes.isEmpty() && refType.isPresent()) { + } else if (!schemaNode.has("properties") + && oneOfTypes.isEmpty() + && allOfTypes.isEmpty() + && refType.isPresent()) { javaType = refType.get(); - } else if (!unionTypes.isEmpty()) { + } else { JPackage container = generatableType.getPackage(); - JType generatedType = - ruleFactory.getTypeRule().apply(nodeName, schemaNode, parent, container, schema); - try { - JDefinedClass unionClass; - Optional commonType; - if (generatedType instanceof JDefinedClass) { - JDefinedClass clazz = (JDefinedClass) generatedType; - if (clazz.methods().isEmpty()) { - unionClass = clazz; - commonType = Optional.empty(); + javaType = ruleFactory.getTypeRule().apply(nodeName, schemaNode, parent, container, schema); + if (javaType instanceof JDefinedClass) { + javaType = + populateAllOf( + schema, populateRef((JDefinedClass) javaType, refType, schema), allOfTypes); + } + if (!oneOfTypes.isEmpty()) { + try { + JDefinedClass unionClass; + Optional commonType; + if (javaType instanceof JDefinedClass) { + JDefinedClass clazz = (JDefinedClass) javaType; + if (clazz.methods().isEmpty()) { + unionClass = clazz; + commonType = Optional.empty(); + } else { + unionClass = container._class(clazz.name() + "Union"); + commonType = Optional.of(clazz); + } } else { - unionClass = container._class(clazz.name() + "Union"); - commonType = Optional.of(clazz); + unionClass = + container._class( + ruleFactory + .getNameHelper() + .getUniqueClassName(nodeName, schemaNode, container)); + commonType = Optional.empty(); } - - } else { - unionClass = - container._class( - ruleFactory.getNameHelper().getUniqueClassName(nodeName, schemaNode, container)); - commonType = Optional.empty(); + javaType = populateOneOf(schema, unionClass, commonType, oneOfTypes); + } catch (JClassAlreadyExistsException ex) { + throw new IllegalStateException(ex); } - javaType = - populateRef(populateClass(schema, unionClass, commonType, unionTypes), refType, schema); - schema.setJavaTypeIfEmpty(javaType); - } catch (JClassAlreadyExistsException ex) { - throw new IllegalStateException(ex); - } - } else { - javaType = - ruleFactory - .getTypeRule() - .apply(nodeName, schemaNode, parent, generatableType.getPackage(), schema); - if (javaType instanceof JDefinedClass) { - populateRef((JDefinedClass) javaType, refType, schema); } schema.setJavaTypeIfEmpty(javaType); } - return javaType; } - private JDefinedClass populateClass( + private JDefinedClass populateAllOf( + Schema parentSchema, JDefinedClass definedClass, Collection allOfTypes) { + return wrapAll(parentSchema, definedClass, Optional.empty(), allOfTypes, Optional.empty()); + } + + private JDefinedClass populateOneOf( Schema parentSchema, JDefinedClass definedClass, Optional commonType, - Collection unionTypes) { + Collection oneOfTypes) { + JFieldVar valueField = definedClass.field( JMod.PRIVATE, @@ -210,9 +216,7 @@ private JDefinedClass populateClass( .owner() .ref(GeneratorUtils.ONE_OF_VALUE_PROVIDER_INTERFACE_NAME) .narrow(valueField.type())); - GeneratorUtils.implementInterface(definedClass, valueField); - try { JDefinedClass serializer = generateSerializer(definedClass); definedClass.annotate(JsonSerialize.class).param("using", serializer); @@ -221,28 +225,33 @@ private JDefinedClass populateClass( } try { - JDefinedClass deserializer = generateDeserializer(definedClass, unionTypes); + JDefinedClass deserializer = + generateDeserializer(definedClass, oneOfTypes, "deserializeOneOf"); definedClass.annotate(JsonDeserialize.class).param("using", deserializer); } catch (JClassAlreadyExistsException ex) { // already deserialized aware } + return wrapAll(parentSchema, definedClass, commonType, oneOfTypes, Optional.of(valueField)); + } + + private JDefinedClass wrapAll( + Schema parentSchema, + JDefinedClass definedClass, + Optional commonType, + Collection types, + Optional valueField) { Collection stringTypes = new ArrayList<>(); - for (JTypeWrapper unionType : unionTypes) { + for (JTypeWrapper unionType : types) { if (isStringType(unionType.getType())) { stringTypes.add(unionType); } else { - if (unionType.getType() instanceof JDefinedClass) { commonType.ifPresent( c -> ((JDefinedClass) unionType.getType())._extends((JDefinedClass) c)); } - wrapIt( - parentSchema, - definedClass, - Optional.of(valueField), - unionType.getType(), - unionType.getNode()); + + wrapIt(parentSchema, definedClass, valueField, unionType.getType(), unionType.getNode()); } } if (!stringTypes.isEmpty()) { @@ -291,7 +300,7 @@ private JDefinedClass generateSerializer(JDefinedClass relatedClass) } private JDefinedClass generateDeserializer( - JDefinedClass relatedClass, Collection unionTypes) + JDefinedClass relatedClass, Collection oneOfTypes, String methodName) throws JClassAlreadyExistsException { JDefinedClass definedClass = GeneratorUtils.deserializerClass(relatedClass); GeneratorUtils.fillDeserializer( @@ -299,20 +308,25 @@ private JDefinedClass generateDeserializer( relatedClass, (method, parserParam) -> { JBlock body = method.body(); - JInvocation list = definedClass.owner().ref(List.class).staticInvoke("of"); - unionTypes.forEach(c -> list.arg(((JClass) c.getType()).dotclass())); + body._return( definedClass .owner() .ref(GeneratorUtils.DESERIALIZE_HELPER_NAME) - .staticInvoke("deserializeOneOf") + .staticInvoke(methodName) .arg(parserParam) .arg(relatedClass.dotclass()) - .arg(list)); + .arg(list(definedClass, oneOfTypes))); }); return definedClass; } + private JInvocation list(JDefinedClass definedClass, Collection list) { + JInvocation result = definedClass.owner().ref(List.class).staticInvoke("of"); + list.forEach(c -> result.arg(((JClass) c.getType()).dotclass())); + return result; + } + private void wrapIt( Schema parentSchema, JDefinedClass definedClass, @@ -320,35 +334,61 @@ private void wrapIt( JType unionType, JsonNode node) { JFieldVar instanceField = getInstanceField(parentSchema, definedClass, unionType, node); - JMethod constructor = definedClass.constructor(JMod.PUBLIC); - JVar instanceParam = constructor.param(unionType, instanceField.name()); - JBlock body = constructor.body(); - valueField.ifPresent(v -> body.assign(JExpr._this().ref(v), instanceParam)); - body.assign(JExpr._this().ref(instanceField), instanceParam); + JMethod method = getSetterMethod(definedClass, instanceField, node); + method + .body() + .assign( + JExpr._this().ref(instanceField), + setupMethod(definedClass, method, valueField, instanceField)); + } + + private JVar setupMethod( + JDefinedClass definedClass, + JMethod method, + Optional valueField, + JFieldVar instanceField) { + JVar methodParam = method.param(instanceField.type(), instanceField.name()); + valueField.ifPresent( + v -> { + method.body().assign(JExpr._this().ref(v), methodParam); + method + .annotate(definedClass.owner().ref(GeneratorUtils.SETTER_ANNOTATION_NAME)) + .param("value", instanceField.type()); + }); + return methodParam; + } + + private JMethod getSetterMethod( + JDefinedClass definedClass, JFieldVar instanceField, JsonNode node) { + String setterName = ruleFactory.getNameHelper().getSetterName(instanceField.name(), node); + JMethod fluentMethod = + definedClass.method(JMod.PUBLIC, definedClass, setterName.replaceFirst("set", "with")); + JBlock body = fluentMethod.body(); + body.assign(instanceField, fluentMethod.param(instanceField.type(), "value")); + body._return(JExpr._this()); + return definedClass.method(JMod.PUBLIC, definedClass.owner().VOID, setterName); } private void wrapStrings( Schema parentSchema, JDefinedClass definedClass, - JFieldVar valueField, + Optional valueField, Collection stringTypes) { Iterator iter = stringTypes.iterator(); JTypeWrapper first = iter.next(); - JMethod constructor = definedClass.constructor(JMod.PUBLIC); - - JBlock body = constructor.body(); String pattern = pattern(first.getNode(), parentSchema); if (pattern == null && iter.hasNext()) { pattern = ".*"; } JFieldVar instanceField = getInstanceField(parentSchema, definedClass, first.getType(), first.getNode()); - JVar instanceParam = constructor.param(first.type, instanceField.name()); - body.assign(JExpr._this().ref(valueField), instanceParam); + JMethod setterMethod = getSetterMethod(definedClass, instanceField, first.getNode()); + JVar methodParam = setupMethod(definedClass, setterMethod, valueField, instanceField); + JBlock body = setterMethod.body(); if (pattern != null) { JConditional condition = - body._if(getPatternCondition(pattern, body, instanceField, instanceParam, definedClass)); - condition._then().assign(JExpr._this().ref(instanceField), instanceParam); + body._if(getPatternCondition(pattern, body, instanceField, methodParam, definedClass)); + condition._then().assign(JExpr._this().ref(instanceField), methodParam); while (iter.hasNext()) { JTypeWrapper item = iter.next(); instanceField = @@ -359,8 +399,8 @@ private void wrapStrings( } condition = condition._elseif( - getPatternCondition(pattern, body, instanceField, instanceParam, definedClass)); - condition._then().assign(JExpr._this().ref(instanceField), instanceParam); + getPatternCondition(pattern, body, instanceField, methodParam, definedClass)); + condition._then().assign(JExpr._this().ref(instanceField), methodParam); } condition ._else() @@ -372,10 +412,10 @@ private void wrapStrings( .ref(String.class) .staticInvoke("format") .arg("%s does not match any pattern") - .arg(instanceParam)) + .arg(methodParam)) .arg(JExpr._null())); } else { - body.assign(JExpr._this().ref(instanceField), instanceParam); + body.assign(JExpr._this().ref(instanceField), methodParam); } } @@ -388,7 +428,7 @@ private JFieldVar getInstanceField( ruleFactory .getNameHelper() .getPropertyName(getTypeName(node, type, parentSchema), node)); - GeneratorUtils.buildMethod( + GeneratorUtils.getterMethod( definedClass, instanceField, ruleFactory.getNameHelper(), instanceField.name()); return instanceField; } diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java index 7248d594..e7af60d5 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java @@ -38,6 +38,8 @@ public class GeneratorUtils { "io.serverlessworkflow.serialization.DeserializeHelper"; public static final String ONE_OF_VALUE_PROVIDER_INTERFACE_NAME = "io.serverlessworkflow.api.OneOfValueProvider"; + public static final String SETTER_ANNOTATION_NAME = + "io.serverlessworkflow.serialization.OneOfSetter"; @FunctionalInterface public interface SerializerFiller { @@ -66,7 +68,7 @@ public static JMethod implementInterface(JDefinedClass definedClass, JFieldVar v return method; } - public static JMethod buildMethod( + public static JMethod getterMethod( JDefinedClass definedClass, JFieldVar instanceField, NameHelper nameHelper, String name) { JMethod method = definedClass.method( diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java index 0e937658..18bdbba6 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java @@ -70,7 +70,7 @@ private JDefinedClass addKeyValueFields( JType stringClass = jclass.owner()._ref(String.class); JFieldVar nameField = jclass.field(JMod.PRIVATE, stringClass, nameHelper.getPropertyName("name", null)); - JMethod nameMethod = GeneratorUtils.buildMethod(jclass, nameField, nameHelper, "name"); + JMethod nameMethod = GeneratorUtils.getterMethod(jclass, nameField, nameHelper, "name"); JType propertyType; if (node != null && node.size() != 0) { String pathToAdditionalProperties; @@ -98,7 +98,7 @@ private JDefinedClass addKeyValueFields( jclass.field( JMod.PRIVATE, propertyType, nameHelper.getPropertyName(propertyType.name(), null)); JMethod valueMethod = - GeneratorUtils.buildMethod(jclass, valueField, nameHelper, propertyType.name()); + GeneratorUtils.getterMethod(jclass, valueField, nameHelper, propertyType.name()); jclass .annotate(JsonSerialize.class) .param("using", generateSerializer(jclass, nameMethod, valueMethod)); From 4fde10fb50106a39a0d434369a222a84dd56c346 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Fri, 15 Nov 2024 16:59:13 +0100 Subject: [PATCH 05/22] Fix schema union not generated for input/output Signed-off-by: Francisco Javier Tirado Sarti --- .../io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java index df3ead34..d14ba357 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java @@ -188,7 +188,7 @@ public JType apply( throw new IllegalStateException(ex); } } - schema.setJavaTypeIfEmpty(javaType); + schema.setJavaType(javaType); } return javaType; } From 52bcbc9e6aba7abcca3bb479f45a9d79f797d538 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:38:16 -0500 Subject: [PATCH 06/22] Bump net.thisptr:jackson-jq from 1.0.1 to 1.1.0 (#471) Bumps [net.thisptr:jackson-jq](https://github.com/eiiches/jackson-jq) from 1.0.1 to 1.1.0. - [Release notes](https://github.com/eiiches/jackson-jq/releases) - [Commits](https://github.com/eiiches/jackson-jq/compare/1.0.1...1.1.0) --- updated-dependencies: - dependency-name: net.thisptr:jackson-jq dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- impl/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/pom.xml b/impl/pom.xml index 3907fb71..770b76ac 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -8,7 +8,7 @@ serverlessworkflow-impl 3.1.9 - 1.0.1 + 1.1.0 From 3c09007841eaee4213ccdfa3d6952919e76ae5c3 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Thu, 14 Nov 2024 19:07:02 +0100 Subject: [PATCH 07/22] [Fix #467] Adding schema validation Signed-off-by: Francisco Javier Tirado Sarti --- impl/pom.xml | 4 + .../impl/WorkflowContext.java | 1 - .../impl/WorkflowDefinition.java | 93 +++++++++++++--- .../impl/WorkflowFactories.java | 56 ++++++++++ .../impl/WorkflowFilter.java | 24 ++++ .../impl/WorkflowUtils.java | 103 +++++++++++++++++ .../impl/executors/AbstractTaskExecutor.java | 90 +++++++-------- .../executors/DefaultTaskExecutorFactory.java | 20 +--- .../impl/executors/HttpExecutor.java | 81 ++++++-------- .../impl/executors/TaskExecutorFactory.java | 3 +- .../impl/expressions/Expression.java | 5 +- .../impl/expressions/ExpressionUtils.java | 8 +- .../impl/expressions/JQExpression.java | 4 +- .../jsonschema/DefaultSchemaValidator.java | 56 ++++++++++ .../DefaultSchemaValidatorFactory.java | 34 ++++++ .../impl/jsonschema/SchemaValidator.java | 22 ++++ .../jsonschema/SchemaValidatorFactory.java | 22 ++++ .../resources/ClasspathResource.java | 37 ++++++ .../resources/DefaultResourceLoader.java | 105 ++++++++++++++++++ .../DefaultResourceLoaderFactory.java | 19 ++++ .../resources/DynamicResource.java | 26 +++++ .../resources/FileResource.java | 45 ++++++++ .../resources/HttpResource.java | 43 +++++++ .../resources/ResourceLoader.java | 28 +++++ .../resources/ResourceLoaderFactory.java | 22 ++++ .../resources/StaticResource.java | 24 ++++ .../impl/WorkflowDefinitionTest.java | 26 +++++ ...http-query-parameters-external-schema.yaml | 18 +++ .../test/resources/schema/searchquery.yaml | 6 + 29 files changed, 886 insertions(+), 139 deletions(-) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/DynamicResource.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/FileResource.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/HttpResource.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java create mode 100644 impl/src/main/java/io/serverlessworkflow/resources/StaticResource.java create mode 100644 impl/src/test/resources/call-http-query-parameters-external-schema.yaml create mode 100644 impl/src/test/resources/schema/searchquery.yaml diff --git a/impl/pom.xml b/impl/pom.xml index 3907fb71..92abc7b6 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -26,6 +26,10 @@ jersey-media-json-jackson ${version.org.glassfish.jersey} + + com.networknt + json-schema-validator + net.thisptr jackson-jq diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java index 6982cfd6..de88d2d0 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java @@ -19,7 +19,6 @@ import io.serverlessworkflow.impl.json.JsonUtils; public class WorkflowContext { - private final WorkflowPosition position; private JsonNode context; private final JsonNode input; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index ec39c90b..afcebeba 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -15,44 +15,79 @@ */ package io.serverlessworkflow.impl; +import static io.serverlessworkflow.impl.WorkflowUtils.*; import static io.serverlessworkflow.impl.json.JsonUtils.*; import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.Input; +import io.serverlessworkflow.api.types.Output; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; import io.serverlessworkflow.impl.executors.TaskExecutor; import io.serverlessworkflow.impl.executors.TaskExecutorFactory; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.JQExpressionFactory; import io.serverlessworkflow.impl.json.JsonUtils; +import io.serverlessworkflow.impl.jsonschema.DefaultSchemaValidatorFactory; +import io.serverlessworkflow.impl.jsonschema.SchemaValidator; +import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.resources.DefaultResourceLoaderFactory; +import io.serverlessworkflow.resources.ResourceLoaderFactory; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; public class WorkflowDefinition { private WorkflowDefinition( Workflow workflow, - TaskExecutorFactory taskFactory, - Collection listeners) { + Collection listeners, + WorkflowFactories factories) { this.workflow = workflow; - this.taskFactory = taskFactory; this.listeners = listeners; + this.factories = factories; + if (workflow.getInput() != null) { + Input input = workflow.getInput(); + this.inputSchemaValidator = + getSchemaValidator( + factories.getValidatorFactory(), schemaToNode(factories, input.getSchema())); + this.inputFilter = buildWorkflowFilter(factories.getExpressionFactory(), input.getFrom()); + } + if (workflow.getOutput() != null) { + Output output = workflow.getOutput(); + this.outputSchemaValidator = + getSchemaValidator( + factories.getValidatorFactory(), schemaToNode(factories, output.getSchema())); + this.outputFilter = buildWorkflowFilter(factories.getExpressionFactory(), output.getAs()); + } } private final Workflow workflow; private final Collection listeners; - private final TaskExecutorFactory taskFactory; + private final WorkflowFactories factories; + private Optional inputSchemaValidator = Optional.empty(); + private Optional outputSchemaValidator = Optional.empty(); + private Optional inputFilter = Optional.empty(); + private Optional outputFilter = Optional.empty(); + private final Map> taskExecutors = new ConcurrentHashMap<>(); public static class Builder { private final Workflow workflow; private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get(); + private ExpressionFactory exprFactory = JQExpressionFactory.get(); private Collection listeners; + private ResourceLoaderFactory resourceLoaderFactory = DefaultResourceLoaderFactory.get(); + private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get(); + private Path path; private Builder(Workflow workflow) { this.workflow = workflow; @@ -71,13 +106,39 @@ public Builder withTaskExecutorFactory(TaskExecutorFactory factory) { return this; } + public Builder withExpressionFactory(ExpressionFactory factory) { + this.exprFactory = factory; + return this; + } + + public Builder withPath(Path path) { + this.path = path; + return this; + } + + public Builder withResourceLoaderFactory(ResourceLoaderFactory resourceLoader) { + this.resourceLoaderFactory = resourceLoader; + return this; + } + + public Builder withSchemaValidatorFactory(SchemaValidatorFactory factory) { + this.schemaValidatorFactory = factory; + return this; + } + public WorkflowDefinition build() { - return new WorkflowDefinition( - workflow, - taskFactory, - listeners == null - ? Collections.emptySet() - : Collections.unmodifiableCollection(listeners)); + WorkflowDefinition def = + new WorkflowDefinition( + workflow, + listeners == null + ? Collections.emptySet() + : Collections.unmodifiableCollection(listeners), + new WorkflowFactories( + taskFactory, + resourceLoaderFactory.getResourceLoader(path), + exprFactory, + schemaValidatorFactory)); + return def; } } @@ -86,7 +147,7 @@ public static Builder builder(Workflow workflow) { } public WorkflowInstance execute(Object input) { - return new WorkflowInstance(taskFactory, JsonUtils.fromValue(input)); + return new WorkflowInstance(JsonUtils.fromValue(input)); } enum State { @@ -101,11 +162,15 @@ public class WorkflowInstance { private State state; private WorkflowContext context; - private WorkflowInstance(TaskExecutorFactory factory, JsonNode input) { + private WorkflowInstance(JsonNode input) { this.output = input; - this.state = State.STARTED; + inputSchemaValidator.ifPresent(v -> v.validate(input)); this.context = WorkflowContext.builder(input).build(); + inputFilter.ifPresent(f -> output = f.apply(context, Optional.empty(), output)); + this.state = State.STARTED; processDo(workflow.getDo()); + outputFilter.ifPresent(f -> output = f.apply(context, Optional.empty(), output)); + outputSchemaValidator.ifPresent(v -> v.validate(output)); } private void processDo(List tasks) { @@ -118,7 +183,7 @@ private void processDo(List tasks) { taskExecutors .computeIfAbsent( context.position().jsonPointer(), - k -> taskFactory.getTaskExecutor(task.getTask())) + k -> factories.getTaskFactory().getTaskExecutor(task.getTask(), factories)) .apply(context, output); listeners.forEach(l -> l.onTaskEnded(context.position(), task.getTask())); context.position().back().back(); diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java new file mode 100644 index 00000000..6b0408b5 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java @@ -0,0 +1,56 @@ +/* + * 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; + +import io.serverlessworkflow.impl.executors.TaskExecutorFactory; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.resources.ResourceLoader; + +public class WorkflowFactories { + + private final TaskExecutorFactory taskFactory; + private final ResourceLoader resourceLoader; + private final ExpressionFactory expressionFactory; + private final SchemaValidatorFactory validatorFactory; + + public WorkflowFactories( + TaskExecutorFactory taskFactory, + ResourceLoader resourceLoader, + ExpressionFactory expressionFactory, + SchemaValidatorFactory validatorFactory) { + this.taskFactory = taskFactory; + this.resourceLoader = resourceLoader; + this.expressionFactory = expressionFactory; + this.validatorFactory = validatorFactory; + } + + public TaskExecutorFactory getTaskFactory() { + return taskFactory; + } + + public ResourceLoader getResourceLoader() { + return resourceLoader; + } + + public ExpressionFactory getExpressionFactory() { + return expressionFactory; + } + + public SchemaValidatorFactory getValidatorFactory() { + return validatorFactory; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java new file mode 100644 index 00000000..7fde97ba --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java @@ -0,0 +1,24 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +import java.util.Optional; + +@FunctionalInterface +public interface WorkflowFilter { + JsonNode apply(WorkflowContext workflow, Optional> task, JsonNode node); +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java new file mode 100644 index 00000000..787d17dc --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -0,0 +1,103 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.serverlessworkflow.api.WorkflowFormat; +import io.serverlessworkflow.api.types.ExportAs; +import io.serverlessworkflow.api.types.InputFrom; +import io.serverlessworkflow.api.types.OutputAs; +import io.serverlessworkflow.api.types.SchemaExternal; +import io.serverlessworkflow.api.types.SchemaInline; +import io.serverlessworkflow.api.types.SchemaUnion; +import io.serverlessworkflow.impl.expressions.Expression; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; +import io.serverlessworkflow.impl.jsonschema.SchemaValidator; +import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.resources.StaticResource; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Optional; + +public class WorkflowUtils { + + private WorkflowUtils() {} + + public static Optional getSchemaValidator( + SchemaValidatorFactory validatorFactory, Optional node) { + return node.map(n -> validatorFactory.getValidator(n)); + } + + public static Optional schemaToNode(WorkflowFactories factories, SchemaUnion schema) { + if (schema != null) { + if (schema.getSchemaInline() != null) { + SchemaInline inline = schema.getSchemaInline(); + return Optional.of(JsonUtils.mapper().convertValue(inline.getDocument(), JsonNode.class)); + } else if (schema.getSchemaExternal() != null) { + SchemaExternal external = schema.getSchemaExternal(); + StaticResource resource = factories.getResourceLoader().loadStatic(external.getResource()); + ObjectMapper mapper = WorkflowFormat.fromFileName(resource.name()).mapper(); + try (InputStream in = resource.open()) { + return Optional.of(mapper.readTree(in)); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + } + return Optional.empty(); + } + + public static Optional buildWorkflowFilter( + ExpressionFactory exprFactory, InputFrom from) { + return from != null + ? Optional.of(buildWorkflowFilter(exprFactory, from.getString(), from.getObject())) + : Optional.empty(); + } + + public static Optional buildWorkflowFilter( + ExpressionFactory exprFactory, OutputAs as) { + return as != null + ? Optional.of(buildWorkflowFilter(exprFactory, as.getString(), as.getObject())) + : Optional.empty(); + } + + public static Optional buildWorkflowFilter( + ExpressionFactory exprFactory, ExportAs as) { + return as != null + ? Optional.of(buildWorkflowFilter(exprFactory, as.getString(), as.getObject())) + : Optional.empty(); + } + + private static WorkflowFilter buildWorkflowFilter( + ExpressionFactory exprFactory, String str, Object object) { + if (str != null) { + Expression expression = exprFactory.getExpression(str); + return expression::eval; + } else { + Object exprObj = ExpressionUtils.buildExpressionObject(object, exprFactory); + return exprObj instanceof Map + ? (w, t, n) -> + JsonUtils.fromValue( + ExpressionUtils.evaluateExpressionMap((Map) exprObj, w, t, n)) + : (w, t, n) -> JsonUtils.fromValue(object); + } + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java index 36dbbf4f..3ed5d6e6 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -15,6 +15,8 @@ */ package io.serverlessworkflow.impl.executors; +import static io.serverlessworkflow.impl.WorkflowUtils.*; + import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.Export; import io.serverlessworkflow.api.types.Input; @@ -22,93 +24,79 @@ import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; -import io.serverlessworkflow.impl.expressions.Expression; -import io.serverlessworkflow.impl.expressions.ExpressionFactory; -import io.serverlessworkflow.impl.expressions.ExpressionUtils; -import io.serverlessworkflow.impl.json.JsonUtils; -import java.util.Map; +import io.serverlessworkflow.impl.WorkflowFactories; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import java.util.Optional; public abstract class AbstractTaskExecutor implements TaskExecutor { protected final T task; - protected final ExpressionFactory exprFactory; - - private interface TaskFilter { - JsonNode apply(WorkflowContext workflow, TaskContext task, JsonNode node); - } - private final Optional> inputProcessor; - private final Optional> outputProcessor; - private final Optional> contextProcessor; + private Optional inputProcessor = Optional.empty(); + private Optional outputProcessor = Optional.empty(); + private Optional contextProcessor = Optional.empty(); + private Optional inputSchemaValidator = Optional.empty(); + private Optional outputSchemaValidator = Optional.empty(); + private Optional contextSchemaValidator = Optional.empty(); - protected AbstractTaskExecutor(T task, ExpressionFactory exprFactory) { + protected AbstractTaskExecutor(T task, WorkflowFactories holder) { this.task = task; - this.exprFactory = exprFactory; - this.inputProcessor = Optional.ofNullable(getInputProcessor()); - this.outputProcessor = Optional.ofNullable(getOutputProcessor()); - this.contextProcessor = Optional.ofNullable(getContextProcessor()); + buildInputProcessors(holder); + buildOutputProcessors(holder); + buildContextProcessors(holder); } - private TaskFilter getInputProcessor() { + private void buildInputProcessors(WorkflowFactories holder) { if (task.getInput() != null) { Input input = task.getInput(); - // TODO add schema validator - if (input.getFrom() != null) { - return getTaskFilter(input.getFrom().getString(), input.getFrom().getObject()); - } + this.inputProcessor = buildWorkflowFilter(holder.getExpressionFactory(), input.getFrom()); + this.inputSchemaValidator = + getSchemaValidator(holder.getValidatorFactory(), schemaToNode(holder, input.getSchema())); } - return null; } - private TaskFilter getOutputProcessor() { + private void buildOutputProcessors(WorkflowFactories holder) { if (task.getOutput() != null) { Output output = task.getOutput(); - // TODO add schema validator - if (output.getAs() != null) { - return getTaskFilter(output.getAs().getString(), output.getAs().getObject()); - } + this.outputProcessor = buildWorkflowFilter(holder.getExpressionFactory(), output.getAs()); + this.outputSchemaValidator = + getSchemaValidator( + holder.getValidatorFactory(), schemaToNode(holder, output.getSchema())); } - return null; } - private TaskFilter getContextProcessor() { + private void buildContextProcessors(WorkflowFactories holder) { if (task.getExport() != null) { Export export = task.getExport(); - // TODO add schema validator if (export.getAs() != null) { - return getTaskFilter(export.getAs().getString(), export.getAs().getObject()); + this.contextProcessor = buildWorkflowFilter(holder.getExpressionFactory(), export.getAs()); } - } - return null; - } - - private TaskFilter getTaskFilter(String str, Object object) { - if (str != null) { - Expression expression = exprFactory.getExpression(str); - return expression::eval; - } else { - Object exprObj = ExpressionUtils.buildExpressionObject(object, exprFactory); - return exprObj instanceof Map - ? (w, t, n) -> - JsonUtils.fromValue( - ExpressionUtils.evaluateExpressionMap((Map) exprObj, w, t, n)) - : (w, t, n) -> JsonUtils.fromValue(object); + this.contextSchemaValidator = + getSchemaValidator( + holder.getValidatorFactory(), schemaToNode(holder, export.getSchema())); } } @Override public JsonNode apply(WorkflowContext workflowContext, JsonNode rawInput) { TaskContext taskContext = new TaskContext<>(rawInput, task); + inputSchemaValidator.ifPresent(s -> s.validate(taskContext.rawInput())); inputProcessor.ifPresent( - p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); + p -> + taskContext.input( + p.apply(workflowContext, Optional.of(taskContext), taskContext.rawInput()))); taskContext.rawOutput(internalExecute(workflowContext, taskContext, taskContext.input())); outputProcessor.ifPresent( - p -> taskContext.output(p.apply(workflowContext, taskContext, taskContext.rawOutput()))); + p -> + taskContext.output( + p.apply(workflowContext, Optional.of(taskContext), taskContext.rawOutput()))); + outputSchemaValidator.ifPresent(s -> s.validate(taskContext.output())); contextProcessor.ifPresent( p -> workflowContext.context( - p.apply(workflowContext, taskContext, workflowContext.context()))); + p.apply(workflowContext, Optional.of(taskContext), workflowContext.context()))); + contextSchemaValidator.ifPresent(s -> s.validate(workflowContext.context())); return taskContext.output(); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index cf49657e..cb76e395 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -18,33 +18,23 @@ import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.impl.expressions.ExpressionFactory; -import io.serverlessworkflow.impl.expressions.JQExpressionFactory; +import io.serverlessworkflow.impl.WorkflowFactories; public class DefaultTaskExecutorFactory implements TaskExecutorFactory { - private final ExpressionFactory exprFactory; - - private static TaskExecutorFactory instance = - new DefaultTaskExecutorFactory(JQExpressionFactory.get()); + private static TaskExecutorFactory instance = new DefaultTaskExecutorFactory(); public static TaskExecutorFactory get() { return instance; } - public static TaskExecutorFactory get(ExpressionFactory factory) { - return new DefaultTaskExecutorFactory(factory); - } - - protected DefaultTaskExecutorFactory(ExpressionFactory exprFactory) { - this.exprFactory = exprFactory; - } + protected DefaultTaskExecutorFactory() {} - public TaskExecutor getTaskExecutor(Task task) { + public TaskExecutor getTaskExecutor(Task task, WorkflowFactories factories) { if (task.getCallTask() != null) { CallTask callTask = task.getCallTask(); if (callTask.getCallHTTP() != null) { - return new HttpExecutor(callTask.getCallHTTP(), exprFactory); + return new HttpExecutor(callTask.getCallHTTP(), factories); } } throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index 60da619c..e17fd8dc 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -24,6 +24,7 @@ import io.serverlessworkflow.api.types.UriTemplate; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowFactories; import io.serverlessworkflow.impl.expressions.Expression; import io.serverlessworkflow.impl.expressions.ExpressionFactory; import io.serverlessworkflow.impl.expressions.ExpressionUtils; @@ -34,9 +35,9 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.Invocation.Builder; import jakarta.ws.rs.client.WebTarget; -import java.net.URI; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; public class HttpExecutor extends AbstractTaskExecutor { @@ -57,28 +58,31 @@ private interface RequestSupplier { JsonNode apply(Builder request, WorkflowContext workflow, TaskContext task, JsonNode node); } - public HttpExecutor(CallHTTP task, ExpressionFactory factory) { - super(task, factory); + public HttpExecutor(CallHTTP task, WorkflowFactories holder) { + super(task, holder); HTTPArguments httpArgs = task.getWith(); - this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint()); + this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint(), holder.getExpressionFactory()); this.headersMap = httpArgs.getHeaders() != null ? ExpressionUtils.buildExpressionMap( - httpArgs.getHeaders().getAdditionalProperties(), factory) + httpArgs.getHeaders().getAdditionalProperties(), holder.getExpressionFactory()) : Map.of(); this.queryMap = httpArgs.getQuery() != null ? ExpressionUtils.buildExpressionMap( - httpArgs.getQuery().getAdditionalProperties(), factory) + httpArgs.getQuery().getAdditionalProperties(), holder.getExpressionFactory()) : Map.of(); switch (httpArgs.getMethod().toUpperCase()) { case HttpMethod.POST: - Object body = ExpressionUtils.buildExpressionObject(httpArgs.getBody(), factory); + Object body = + ExpressionUtils.buildExpressionObject( + httpArgs.getBody(), holder.getExpressionFactory()); this.requestFunction = (request, workflow, context, node) -> request.post( Entity.json( - ExpressionUtils.evaluateExpressionObject(body, workflow, context, node)), + ExpressionUtils.evaluateExpressionObject( + body, workflow, Optional.of(context), node)), JsonNode.class); break; case HttpMethod.GET: @@ -92,79 +96,58 @@ protected JsonNode internalExecute( WorkflowContext workflow, TaskContext taskContext, JsonNode input) { WebTarget target = targetSupplier.apply(workflow, taskContext, input); for (Entry entry : - ExpressionUtils.evaluateExpressionMap(queryMap, workflow, taskContext, input).entrySet()) { + ExpressionUtils.evaluateExpressionMap(queryMap, workflow, Optional.of(taskContext), input) + .entrySet()) { target = target.queryParam(entry.getKey(), entry.getValue()); } Builder request = target.request(); - ExpressionUtils.evaluateExpressionMap(headersMap, workflow, taskContext, input) + ExpressionUtils.evaluateExpressionMap(headersMap, workflow, Optional.of(taskContext), input) .forEach(request::header); return requestFunction.apply(request, workflow, taskContext, input); } - private TargetSupplier getTargetSupplier(Endpoint endpoint) { + private static TargetSupplier getTargetSupplier( + Endpoint endpoint, ExpressionFactory expressionFactory) { if (endpoint.getEndpointConfiguration() != null) { EndpointUri uri = endpoint.getEndpointConfiguration().getUri(); if (uri.getLiteralEndpointURI() != null) { return getURISupplier(uri.getLiteralEndpointURI()); } else if (uri.getExpressionEndpointURI() != null) { - return new ExpressionURISupplier(uri.getExpressionEndpointURI()); + return new ExpressionURISupplier( + expressionFactory.getExpression(uri.getExpressionEndpointURI())); } } else if (endpoint.getRuntimeExpression() != null) { - return new ExpressionURISupplier(endpoint.getRuntimeExpression()); + return new ExpressionURISupplier( + expressionFactory.getExpression(endpoint.getRuntimeExpression())); } else if (endpoint.getUriTemplate() != null) { return getURISupplier(endpoint.getUriTemplate()); } throw new IllegalArgumentException("Invalid endpoint definition " + endpoint); } - private TargetSupplier getURISupplier(UriTemplate template) { + private static TargetSupplier getURISupplier(UriTemplate template) { if (template.getLiteralUri() != null) { - return new URISupplier(template.getLiteralUri()); + return (w, t, n) -> client.target(template.getLiteralUri()); } else if (template.getLiteralUriTemplate() != null) { - return new URITemplateSupplier(template.getLiteralUriTemplate()); + return (w, t, n) -> + client + .target(template.getLiteralUriTemplate()) + .resolveTemplates( + JsonUtils.mapper().convertValue(n, new TypeReference>() {})); } throw new IllegalArgumentException("Invalid uritemplate definition " + template); } - private class URISupplier implements TargetSupplier { - private final URI uri; - - public URISupplier(URI uri) { - this.uri = uri; - } - - @Override - public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { - return client.target(uri); - } - } - - private class URITemplateSupplier implements TargetSupplier { - private final String uri; - - public URITemplateSupplier(String uri) { - this.uri = uri; - } - - @Override - public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { - return client - .target(uri) - .resolveTemplates( - JsonUtils.mapper().convertValue(node, new TypeReference>() {})); - } - } - - private class ExpressionURISupplier implements TargetSupplier { + private static class ExpressionURISupplier implements TargetSupplier { private Expression expr; - public ExpressionURISupplier(String expr) { - this.expr = exprFactory.getExpression(expr); + public ExpressionURISupplier(Expression expr) { + this.expr = expr; } @Override public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { - return client.target(expr.eval(workflow, task, node).asText()); + return client.target(expr.eval(workflow, Optional.of(task), node).asText()); } } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java index 3a9068c3..85cef4b1 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java @@ -17,7 +17,8 @@ import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.WorkflowFactories; public interface TaskExecutorFactory { - TaskExecutor getTaskExecutor(Task task); + TaskExecutor getTaskExecutor(Task task, WorkflowFactories factories); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java index 37206712..f9d799ec 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java @@ -16,11 +16,10 @@ package io.serverlessworkflow.impl.expressions; import com.fasterxml.jackson.databind.JsonNode; -import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; +import java.util.Optional; public interface Expression { - JsonNode eval( - WorkflowContext workflowContext, TaskContext context, JsonNode node); + JsonNode eval(WorkflowContext workflowContext, Optional> context, JsonNode node); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java index 7f776322..72b4e90c 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java @@ -20,6 +20,7 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.json.JsonUtils; import java.util.Map; +import java.util.Optional; public class ExpressionUtils { @@ -34,7 +35,10 @@ public static Map buildExpressionMap( } public static Map evaluateExpressionMap( - Map origMap, WorkflowContext workflow, TaskContext task, JsonNode n) { + Map origMap, + WorkflowContext workflow, + Optional> task, + JsonNode n) { return new ProxyMap( origMap, o -> @@ -50,7 +54,7 @@ public static Object buildExpressionObject(Object obj, ExpressionFactory factory } public static Object evaluateExpressionObject( - Object obj, WorkflowContext workflow, TaskContext task, JsonNode node) { + Object obj, WorkflowContext workflow, Optional> task, JsonNode node) { return obj instanceof Map ? ExpressionUtils.evaluateExpressionMap((Map) obj, workflow, task, node) : obj; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java index 2e64e17a..7b008fcb 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.json.JsonUtils; @@ -178,8 +177,7 @@ public JsonNode getResult() { } @Override - public JsonNode eval( - WorkflowContext workflow, TaskContext task, JsonNode node) { + public JsonNode eval(WorkflowContext workflow, Optional> task, JsonNode node) { TypedOutput output = output(JsonNode.class); try { internalExpr.apply(this.scope.get(), node, output); diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java new file mode 100644 index 00000000..232926b3 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.serverlessworkflow.impl.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.ValidationMessage; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class DefaultSchemaValidator implements SchemaValidator { + + private final JsonNode jsonNode; + private final AtomicReference schemaObject = new AtomicReference<>(); + + public DefaultSchemaValidator(JsonNode jsonNode) { + this.jsonNode = jsonNode; + } + + @Override + public void validate(JsonNode node) { + Set report = getSchema().validate(node); + if (!report.isEmpty()) { + StringBuilder sb = new StringBuilder("There are JsonSchema validation errors:"); + report.forEach(m -> sb.append(System.lineSeparator()).append(m.getMessage())); + throw new IllegalArgumentException(sb.toString()); + } + } + + private JsonSchema getSchema() { + JsonSchema result = schemaObject.get(); + if (result == null) { + result = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(jsonNode); + schemaObject.set(result); + } + return result; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java new file mode 100644 index 00000000..0f74e433 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java @@ -0,0 +1,34 @@ +/* + * 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.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; + +public class DefaultSchemaValidatorFactory implements SchemaValidatorFactory { + + private DefaultSchemaValidatorFactory() {} + + private static final DefaultSchemaValidatorFactory instance = new DefaultSchemaValidatorFactory(); + + public static DefaultSchemaValidatorFactory get() { + return instance; + } + + @Override + public SchemaValidator getValidator(JsonNode node) { + return new DefaultSchemaValidator(node); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java new file mode 100644 index 00000000..d86a582f --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java @@ -0,0 +1,22 @@ +/* + * 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.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface SchemaValidator { + void validate(JsonNode node); +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java new file mode 100644 index 00000000..52c29584 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java @@ -0,0 +1,22 @@ +/* + * 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.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface SchemaValidatorFactory { + SchemaValidator getValidator(JsonNode node); +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java b/impl/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java new file mode 100644 index 00000000..81455712 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java @@ -0,0 +1,37 @@ +/* + * 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.resources; + +import java.io.InputStream; + +public class ClasspathResource implements StaticResource { + + private String path; + + public ClasspathResource(String path) { + this.path = path; + } + + @Override + public InputStream open() { + return Thread.currentThread().getContextClassLoader().getResourceAsStream(path); + } + + @Override + public String name() { + return path; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java b/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java new file mode 100644 index 00000000..be6015b8 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java @@ -0,0 +1,105 @@ +/* + * 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.resources; + +import io.serverlessworkflow.api.types.Endpoint; +import io.serverlessworkflow.api.types.EndpointUri; +import io.serverlessworkflow.api.types.ExternalResource; +import io.serverlessworkflow.api.types.UriTemplate; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.file.Path; +import java.util.Optional; + +public class DefaultResourceLoader implements ResourceLoader { + + private final Optional workflowPath; + + protected DefaultResourceLoader(Path workflowPath) { + this.workflowPath = Optional.ofNullable(workflowPath); + } + + @Override + public StaticResource loadStatic(ExternalResource resource) { + return processEndpoint(resource.getEndpoint()); + } + + @Override + public DynamicResource loadDynamic( + WorkflowContext workflow, ExternalResource resource, ExpressionFactory factory) { + throw new UnsupportedOperationException("Dynamic loading of resources is not suppported"); + } + + private StaticResource buildFromString(String uri) { + return fileResource(uri); + } + + private StaticResource fileResource(String pathStr) { + Path path = Path.of(pathStr); + if (path.isAbsolute()) { + return new FileResource(path); + } else { + return workflowPath + .map(p -> new FileResource(p.resolve(path))) + .orElseGet(() -> new ClasspathResource(pathStr)); + } + } + + private StaticResource buildFromURI(URI uri) { + String scheme = uri.getScheme(); + if (scheme == null || scheme.equalsIgnoreCase("file")) { + return fileResource(uri.getPath()); + } else if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) { + try { + return new HttpResource(uri.toURL()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } else { + throw new UnsupportedOperationException("Unsupported scheme " + scheme); + } + } + + private StaticResource processEndpoint(Endpoint endpoint) { + if (endpoint.getEndpointConfiguration() != null) { + EndpointUri uri = endpoint.getEndpointConfiguration().getUri(); + if (uri.getLiteralEndpointURI() != null) { + return getURI(uri.getLiteralEndpointURI()); + } else if (uri.getExpressionEndpointURI() != null) { + throw new UnsupportedOperationException( + "Expression not supported for loading a static resource"); + } + } else if (endpoint.getRuntimeExpression() != null) { + throw new UnsupportedOperationException( + "Expression not supported for loading a static resource"); + } else if (endpoint.getUriTemplate() != null) { + return getURI(endpoint.getUriTemplate()); + } + throw new IllegalArgumentException("Invalid endpoint definition " + endpoint); + } + + private StaticResource getURI(UriTemplate template) { + if (template.getLiteralUri() != null) { + return buildFromURI(template.getLiteralUri()); + } else if (template.getLiteralUriTemplate() != null) { + return buildFromString(template.getLiteralUriTemplate()); + } else { + throw new IllegalStateException("Invalid endpoint definition" + template); + } + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java b/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java new file mode 100644 index 00000000..5a33601b --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java @@ -0,0 +1,19 @@ +package io.serverlessworkflow.resources; + +import java.nio.file.Path; + +public class DefaultResourceLoaderFactory implements ResourceLoaderFactory { + + public static final ResourceLoaderFactory get() { + return factory; + } + + private static final ResourceLoaderFactory factory = new DefaultResourceLoaderFactory(); + + private DefaultResourceLoaderFactory() {} + + @Override + public ResourceLoader getResourceLoader(Path path) { + return new DefaultResourceLoader(path); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/DynamicResource.java b/impl/src/main/java/io/serverlessworkflow/resources/DynamicResource.java new file mode 100644 index 00000000..476f24e7 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/DynamicResource.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.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import java.io.InputStream; +import java.util.Optional; + +public interface DynamicResource { + InputStream open(WorkflowContext workflow, Optional> task, JsonNode input); +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/FileResource.java b/impl/src/main/java/io/serverlessworkflow/resources/FileResource.java new file mode 100644 index 00000000..a8b54ff7 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/FileResource.java @@ -0,0 +1,45 @@ +/* + * 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.resources; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +class FileResource implements StaticResource { + + private Path path; + + public FileResource(Path path) { + this.path = path; + } + + @Override + public InputStream open() { + try { + return Files.newInputStream(path); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + @Override + public String name() { + return path.getFileName().toString(); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/HttpResource.java b/impl/src/main/java/io/serverlessworkflow/resources/HttpResource.java new file mode 100644 index 00000000..27d64a94 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/HttpResource.java @@ -0,0 +1,43 @@ +/* + * 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.resources; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; + +public class HttpResource implements StaticResource { + + private URL url; + + public HttpResource(URL url) { + this.url = url; + } + + @Override + public InputStream open() { + try { + return url.openStream(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public String name() { + return url.getFile(); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java b/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java new file mode 100644 index 00000000..1e6ff2d5 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java @@ -0,0 +1,28 @@ +/* + * 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.resources; + +import io.serverlessworkflow.api.types.ExternalResource; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; + +public interface ResourceLoader { + + StaticResource loadStatic(ExternalResource resource); + + DynamicResource loadDynamic( + WorkflowContext context, ExternalResource resource, ExpressionFactory factory); +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java b/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java new file mode 100644 index 00000000..56347041 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java @@ -0,0 +1,22 @@ +/* + * 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.resources; + +import java.nio.file.Path; + +public interface ResourceLoaderFactory { + ResourceLoader getResourceLoader(Path path); +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/StaticResource.java b/impl/src/main/java/io/serverlessworkflow/resources/StaticResource.java new file mode 100644 index 00000000..3b52a79c --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/resources/StaticResource.java @@ -0,0 +1,24 @@ +/* + * 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.resources; + +import java.io.InputStream; + +public interface StaticResource { + InputStream open(); + + String name(); +} diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index ba842e4e..c1dbd85f 100644 --- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -17,6 +17,7 @@ */ import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; import java.io.IOException; import java.util.Map; @@ -25,6 +26,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; public class WorkflowDefinitionTest { @@ -40,6 +42,25 @@ void testWorkflowExecution(String fileName, Object input, Condition cond .is(condition); } + @ParameterizedTest + @ValueSource( + strings = { + "call-http-query-parameters.yaml", + "call-http-query-parameters-external-schema.yaml" + }) + void testWrongSchema(String fileName) { + IllegalArgumentException exception = + catchThrowableOfType( + IllegalArgumentException.class, + () -> + WorkflowDefinition.builder(readWorkflowFromClasspath(fileName)) + .build() + .execute(Map.of())); + assertThat(exception) + .isNotNull() + .hasMessageContaining("There are JsonSchema validation errors"); + } + private static Stream provideParameters() { Map petInput = Map.of("petId", 10); Condition petCondition = @@ -53,6 +74,11 @@ private static Stream provideParameters() { Map.of("searchQuery", "R2-D2"), new Condition<>( o -> ((Map) o).get("count").equals(1), "R2D2Condition")), + Arguments.of( + "call-http-query-parameters-external-schema.yaml", + Map.of("searchQuery", "Luke Skywalker"), + new Condition<>( + o -> ((Map) o).get("count").equals(1), "TheRealJediCondition")), Arguments.of( "callPostHttp.yaml", Map.of("name", "Javierito", "status", "available"), diff --git a/impl/src/test/resources/call-http-query-parameters-external-schema.yaml b/impl/src/test/resources/call-http-query-parameters-external-schema.yaml new file mode 100644 index 00000000..a5bb1437 --- /dev/null +++ b/impl/src/test/resources/call-http-query-parameters-external-schema.yaml @@ -0,0 +1,18 @@ +document: + dsl: 1.0.0-alpha2 + namespace: examples + name: http-query-params + version: 1.0.0-alpha2 +input: + schema: + resource: + endpoint: schema/searchquery.yaml +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + diff --git a/impl/src/test/resources/schema/searchquery.yaml b/impl/src/test/resources/schema/searchquery.yaml new file mode 100644 index 00000000..f6dde131 --- /dev/null +++ b/impl/src/test/resources/schema/searchquery.yaml @@ -0,0 +1,6 @@ +type: object +required: + - searchQuery +properties: + searchQuery: + type: string \ No newline at end of file From 0e3a8899c0207e914fc32abd3fa8f2103c84977a Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Fri, 22 Nov 2024 17:06:40 +0100 Subject: [PATCH 08/22] [Fix #466] Implement switch Signed-off-by: Francisco Javier Tirado Sarti --- .github/workflows/maven-verify.yml | 4 +- .github/workflows/release.yml | 6 +- .../serverlessworkflow/impl/TaskContext.java | 20 +- .../impl/WorkflowApplication.java | 148 +++++++++++++ .../impl/WorkflowContext.java | 31 ++- .../impl/WorkflowDefinition.java | 209 +++++++----------- .../impl/WorkflowFactories.java | 56 ----- .../impl/WorkflowInstance.java | 52 +++++ .../impl/WorkflowState.java | 7 + .../impl/WorkflowUtils.java | 90 +++++++- .../impl/executors/AbstractTaskExecutor.java | 41 ++-- .../executors/DefaultTaskExecutorFactory.java | 13 +- .../impl/executors/DoExecutor.java | 34 +++ .../impl/executors/HttpExecutor.java | 20 +- .../impl/executors/SetExecutor.java | 39 ++++ .../impl/executors/SwitchExecutor.java | 50 +++++ .../impl/executors/TaskExecutor.java | 3 +- .../impl/executors/TaskExecutorFactory.java | 4 +- .../impl/WorkflowDefinitionTest.java | 54 ++++- .../call-http-endpoint-interpolation.yaml | 2 +- ...http-query-parameters-external-schema.yaml | 2 +- impl/src/test/resources/callGetHttp.yaml | 4 +- impl/src/test/resources/callPostHttp.yaml | 2 +- .../test/resources/switch-then-string.yaml | 45 ++++ pom.xml | 2 +- 25 files changed, 680 insertions(+), 258 deletions(-) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java delete mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java create mode 100644 impl/src/test/resources/switch-then-string.yaml diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 1b7d432c..12ab91f8 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -16,11 +16,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: 'maven' - name: Verify with Maven diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef4ee698..99b13727 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,11 +29,11 @@ jobs: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 cache: 'maven' server-id: ossrh server-username: MAVEN_USERNAME @@ -57,4 +57,4 @@ jobs: MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - name: Push tags - run: git push && git push --tags \ No newline at end of file + run: git push && git push --tags diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java index c9e28f12..91f6aa61 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -16,6 +16,8 @@ package io.serverlessworkflow.impl; import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.FlowDirective; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.TaskBase; public class TaskContext { @@ -26,11 +28,15 @@ public class TaskContext { private JsonNode input; private JsonNode output; private JsonNode rawOutput; + private FlowDirective flowDirective; public TaskContext(JsonNode rawInput, T task) { this.rawInput = rawInput; this.input = rawInput; + this.rawOutput = rawInput; + this.output = rawInput; this.task = task; + this.flowDirective = task.getThen(); } public void input(JsonNode input) { @@ -54,6 +60,10 @@ public void rawOutput(JsonNode output) { this.output = output; } + public JsonNode rawOutput() { + return rawOutput; + } + public void output(JsonNode output) { this.output = output; } @@ -62,7 +72,13 @@ public JsonNode output() { return output; } - public JsonNode rawOutput() { - return rawOutput; + public void flowDirective(FlowDirective flowDirective) { + this.flowDirective = flowDirective; + } + + public FlowDirective flowDirective() { + return flowDirective == null + ? new FlowDirective().withFlowDirectiveEnum(FlowDirectiveEnum.CONTINUE) + : flowDirective; } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java new file mode 100644 index 00000000..3fd81b00 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java @@ -0,0 +1,148 @@ +/* + * 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; + +import io.serverlessworkflow.api.types.Document; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; +import io.serverlessworkflow.impl.executors.TaskExecutorFactory; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.JQExpressionFactory; +import io.serverlessworkflow.impl.jsonschema.DefaultSchemaValidatorFactory; +import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.resources.DefaultResourceLoaderFactory; +import io.serverlessworkflow.resources.ResourceLoaderFactory; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class WorkflowApplication implements AutoCloseable { + + private final TaskExecutorFactory taskFactory; + private final ExpressionFactory exprFactory; + private final ResourceLoaderFactory resourceLoaderFactory; + private final SchemaValidatorFactory schemaValidatorFactory; + private final Collection listeners; + private final Map definitions; + + public WorkflowApplication( + TaskExecutorFactory taskFactory, + ExpressionFactory exprFactory, + ResourceLoaderFactory resourceLoaderFactory, + SchemaValidatorFactory schemaValidatorFactory, + Collection listeners) { + this.taskFactory = taskFactory; + this.exprFactory = exprFactory; + this.resourceLoaderFactory = resourceLoaderFactory; + this.schemaValidatorFactory = schemaValidatorFactory; + this.listeners = listeners; + this.definitions = new ConcurrentHashMap<>(); + } + + public TaskExecutorFactory taskFactory() { + return taskFactory; + } + + public static Builder builder() { + return new Builder(); + } + + public ExpressionFactory expressionFactory() { + return exprFactory; + } + + public SchemaValidatorFactory validatorFactory() { + return schemaValidatorFactory; + } + + public ResourceLoaderFactory resourceLoaderFactory() { + return resourceLoaderFactory; + } + + public Collection listeners() { + return listeners; + } + + public static class Builder { + private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get(); + private ExpressionFactory exprFactory = JQExpressionFactory.get(); + private Collection listeners; + private ResourceLoaderFactory resourceLoaderFactory = DefaultResourceLoaderFactory.get(); + private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get(); + + private Builder() {} + + public Builder withListener(WorkflowExecutionListener listener) { + if (listeners == null) { + listeners = new HashSet<>(); + } + listeners.add(listener); + return this; + } + + public Builder withTaskExecutorFactory(TaskExecutorFactory factory) { + this.taskFactory = factory; + return this; + } + + public Builder withExpressionFactory(ExpressionFactory factory) { + this.exprFactory = factory; + return this; + } + + public Builder withResourceLoaderFactory(ResourceLoaderFactory resourceLoader) { + this.resourceLoaderFactory = resourceLoader; + return this; + } + + public Builder withSchemaValidatorFactory(SchemaValidatorFactory factory) { + this.schemaValidatorFactory = factory; + return this; + } + + public WorkflowApplication build() { + return new WorkflowApplication( + taskFactory, + exprFactory, + resourceLoaderFactory, + schemaValidatorFactory, + listeners == null + ? Collections.emptySet() + : Collections.unmodifiableCollection(listeners)); + } + } + + private static record WorkflowId(String namespace, String name, String version) { + static WorkflowId of(Document document) { + return new WorkflowId(document.getNamespace(), document.getName(), document.getVersion()); + } + } + + public WorkflowDefinition workflowDefinition(Workflow workflow) { + return definitions.computeIfAbsent( + WorkflowId.of(workflow.getDocument()), k -> WorkflowDefinition.of(this, workflow)); + } + + @Override + public void close() throws Exception { + for (WorkflowDefinition definition : definitions.values()) { + definition.close(); + } + definitions.clear(); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java index de88d2d0..4f0f0f16 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java @@ -20,24 +20,31 @@ public class WorkflowContext { private final WorkflowPosition position; - private JsonNode context; + private final WorkflowDefinition definition; private final JsonNode input; + private JsonNode current; + private JsonNode context; - private WorkflowContext(WorkflowPosition position, JsonNode input) { + private WorkflowContext( + WorkflowPosition position, WorkflowDefinition definition, JsonNode input) { this.position = position; + this.definition = definition; this.input = input; + this.current = input.deepCopy(); this.context = JsonUtils.mapper().createObjectNode(); } - public static Builder builder(JsonNode input) { - return new Builder(input); + public static Builder builder(WorkflowDefinition definition, JsonNode input) { + return new Builder(definition, input); } public static class Builder { private WorkflowPosition position = new DefaultWorkflowPosition(); + private WorkflowDefinition definition; private JsonNode input; - private Builder(JsonNode input) { + private Builder(WorkflowDefinition definition, JsonNode input) { + this.definition = definition; this.input = input; } @@ -47,7 +54,7 @@ public Builder position(WorkflowPosition position) { } public WorkflowContext build() { - return new WorkflowContext(position, input); + return new WorkflowContext(position, definition, input); } } @@ -66,4 +73,16 @@ public void context(JsonNode context) { public JsonNode rawInput() { return input; } + + public void current(JsonNode output) { + this.current = output; + } + + public JsonNode current() { + return current; + } + + public WorkflowDefinition definition() { + return definition; + } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index afcebeba..136d2853 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -16,190 +16,133 @@ package io.serverlessworkflow.impl; import static io.serverlessworkflow.impl.WorkflowUtils.*; -import static io.serverlessworkflow.impl.json.JsonUtils.*; -import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.Output; import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; import io.serverlessworkflow.impl.executors.TaskExecutor; import io.serverlessworkflow.impl.executors.TaskExecutorFactory; import io.serverlessworkflow.impl.expressions.ExpressionFactory; -import io.serverlessworkflow.impl.expressions.JQExpressionFactory; import io.serverlessworkflow.impl.json.JsonUtils; -import io.serverlessworkflow.impl.jsonschema.DefaultSchemaValidatorFactory; import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; -import io.serverlessworkflow.resources.DefaultResourceLoaderFactory; -import io.serverlessworkflow.resources.ResourceLoaderFactory; +import io.serverlessworkflow.resources.ResourceLoader; import java.nio.file.Path; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -public class WorkflowDefinition { +public class WorkflowDefinition implements AutoCloseable { + + private final Workflow workflow; + private final Collection listeners; + private Optional inputSchemaValidator = Optional.empty(); + private Optional outputSchemaValidator = Optional.empty(); + private Optional inputFilter = Optional.empty(); + private Optional outputFilter = Optional.empty(); + private final TaskExecutorFactory taskFactory; + private final ExpressionFactory exprFactory; + private final ResourceLoader resourceLoader; + private final SchemaValidatorFactory schemaValidatorFactory; + private final Map> taskExecutors = + new ConcurrentHashMap<>(); private WorkflowDefinition( Workflow workflow, Collection listeners, - WorkflowFactories factories) { + TaskExecutorFactory taskFactory, + ResourceLoader resourceLoader, + ExpressionFactory exprFactory, + SchemaValidatorFactory schemaValidatorFactory) { this.workflow = workflow; this.listeners = listeners; - this.factories = factories; + this.taskFactory = taskFactory; + this.exprFactory = exprFactory; + this.schemaValidatorFactory = schemaValidatorFactory; + this.resourceLoader = resourceLoader; if (workflow.getInput() != null) { Input input = workflow.getInput(); this.inputSchemaValidator = getSchemaValidator( - factories.getValidatorFactory(), schemaToNode(factories, input.getSchema())); - this.inputFilter = buildWorkflowFilter(factories.getExpressionFactory(), input.getFrom()); + schemaValidatorFactory, schemaToNode(resourceLoader, input.getSchema())); + this.inputFilter = buildWorkflowFilter(exprFactory, input.getFrom()); } if (workflow.getOutput() != null) { Output output = workflow.getOutput(); this.outputSchemaValidator = getSchemaValidator( - factories.getValidatorFactory(), schemaToNode(factories, output.getSchema())); - this.outputFilter = buildWorkflowFilter(factories.getExpressionFactory(), output.getAs()); + schemaValidatorFactory, schemaToNode(resourceLoader, output.getSchema())); + this.outputFilter = buildWorkflowFilter(exprFactory, output.getAs()); } } - private final Workflow workflow; - private final Collection listeners; - private final WorkflowFactories factories; - private Optional inputSchemaValidator = Optional.empty(); - private Optional outputSchemaValidator = Optional.empty(); - private Optional inputFilter = Optional.empty(); - private Optional outputFilter = Optional.empty(); + static WorkflowDefinition of(WorkflowApplication application, Workflow workflow) { + return of(application, workflow, null); + } - private final Map> taskExecutors = - new ConcurrentHashMap<>(); + static WorkflowDefinition of(WorkflowApplication application, Workflow workflow, Path path) { + return new WorkflowDefinition( + workflow, + application.listeners(), + application.taskFactory(), + application.resourceLoaderFactory().getResourceLoader(path), + application.expressionFactory(), + application.validatorFactory()); + } - public static class Builder { - private final Workflow workflow; - private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get(); - private ExpressionFactory exprFactory = JQExpressionFactory.get(); - private Collection listeners; - private ResourceLoaderFactory resourceLoaderFactory = DefaultResourceLoaderFactory.get(); - private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get(); - private Path path; - - private Builder(Workflow workflow) { - this.workflow = workflow; - } + public WorkflowInstance execute(Object input) { + return new WorkflowInstance(this, JsonUtils.fromValue(input)); + } - public Builder withListener(WorkflowExecutionListener listener) { - if (listeners == null) { - listeners = new HashSet<>(); - } - listeners.add(listener); - return this; - } + public Optional inputSchemaValidator() { + return inputSchemaValidator; + } - public Builder withTaskExecutorFactory(TaskExecutorFactory factory) { - this.taskFactory = factory; - return this; - } + public Optional inputFilter() { + return inputFilter; + } - public Builder withExpressionFactory(ExpressionFactory factory) { - this.exprFactory = factory; - return this; - } + public Workflow workflow() { + return workflow; + } - public Builder withPath(Path path) { - this.path = path; - return this; - } + public Collection listeners() { + return listeners; + } - public Builder withResourceLoaderFactory(ResourceLoaderFactory resourceLoader) { - this.resourceLoaderFactory = resourceLoader; - return this; - } + public Map> taskExecutors() { + return taskExecutors; + } - public Builder withSchemaValidatorFactory(SchemaValidatorFactory factory) { - this.schemaValidatorFactory = factory; - return this; - } + public TaskExecutorFactory taskFactory() { + return taskFactory; + } - public WorkflowDefinition build() { - WorkflowDefinition def = - new WorkflowDefinition( - workflow, - listeners == null - ? Collections.emptySet() - : Collections.unmodifiableCollection(listeners), - new WorkflowFactories( - taskFactory, - resourceLoaderFactory.getResourceLoader(path), - exprFactory, - schemaValidatorFactory)); - return def; - } + public Optional outputFilter() { + return outputFilter; } - public static Builder builder(Workflow workflow) { - return new Builder(workflow); + public Optional outputSchemaValidator() { + return outputSchemaValidator; } - public WorkflowInstance execute(Object input) { - return new WorkflowInstance(JsonUtils.fromValue(input)); - } - - enum State { - STARTED, - WAITING, - FINISHED - }; - - public class WorkflowInstance { - - private JsonNode output; - private State state; - private WorkflowContext context; - - private WorkflowInstance(JsonNode input) { - this.output = input; - inputSchemaValidator.ifPresent(v -> v.validate(input)); - this.context = WorkflowContext.builder(input).build(); - inputFilter.ifPresent(f -> output = f.apply(context, Optional.empty(), output)); - this.state = State.STARTED; - processDo(workflow.getDo()); - outputFilter.ifPresent(f -> output = f.apply(context, Optional.empty(), output)); - outputSchemaValidator.ifPresent(v -> v.validate(output)); - } + public ExpressionFactory expressionFactory() { + return exprFactory; + } - private void processDo(List tasks) { - context.position().addProperty("do"); - int index = 0; - for (TaskItem task : tasks) { - context.position().addIndex(++index).addProperty(task.getName()); - listeners.forEach(l -> l.onTaskStarted(context.position(), task.getTask())); - this.output = - taskExecutors - .computeIfAbsent( - context.position().jsonPointer(), - k -> factories.getTaskFactory().getTaskExecutor(task.getTask(), factories)) - .apply(context, output); - listeners.forEach(l -> l.onTaskEnded(context.position(), task.getTask())); - context.position().back().back(); - } - } + public SchemaValidatorFactory validatorFactory() { + return schemaValidatorFactory; + } - public State state() { - return state; - } + public ResourceLoader resourceLoader() { - public Object output() { - return toJavaValue(output); - } + return resourceLoader; + } - public Object outputAsJsonNode() { - return output; - } + @Override + public void close() { + // TODO close resourcers hold for uncompleted process instances, if any } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java deleted file mode 100644 index 6b0408b5..00000000 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFactories.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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; - -import io.serverlessworkflow.impl.executors.TaskExecutorFactory; -import io.serverlessworkflow.impl.expressions.ExpressionFactory; -import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; -import io.serverlessworkflow.resources.ResourceLoader; - -public class WorkflowFactories { - - private final TaskExecutorFactory taskFactory; - private final ResourceLoader resourceLoader; - private final ExpressionFactory expressionFactory; - private final SchemaValidatorFactory validatorFactory; - - public WorkflowFactories( - TaskExecutorFactory taskFactory, - ResourceLoader resourceLoader, - ExpressionFactory expressionFactory, - SchemaValidatorFactory validatorFactory) { - this.taskFactory = taskFactory; - this.resourceLoader = resourceLoader; - this.expressionFactory = expressionFactory; - this.validatorFactory = validatorFactory; - } - - public TaskExecutorFactory getTaskFactory() { - return taskFactory; - } - - public ResourceLoader getResourceLoader() { - return resourceLoader; - } - - public ExpressionFactory getExpressionFactory() { - return expressionFactory; - } - - public SchemaValidatorFactory getValidatorFactory() { - return validatorFactory; - } -} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java new file mode 100644 index 00000000..bd2f94b8 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -0,0 +1,52 @@ +/* + * 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; + +import static io.serverlessworkflow.impl.json.JsonUtils.toJavaValue; + +import com.fasterxml.jackson.databind.JsonNode; +import java.util.Optional; + +public class WorkflowInstance { + private WorkflowState state; + private WorkflowContext context; + + WorkflowInstance(WorkflowDefinition definition, JsonNode input) { + definition.inputSchemaValidator().ifPresent(v -> v.validate(input)); + context = WorkflowContext.builder(definition, input).build(); + definition + .inputFilter() + .ifPresent(f -> context.current(f.apply(context, Optional.empty(), context.current()))); + state = WorkflowState.STARTED; + WorkflowUtils.processTaskList(definition.workflow().getDo(), context); + definition + .outputFilter() + .ifPresent(f -> context.current(f.apply(context, Optional.empty(), context.current()))); + definition.outputSchemaValidator().ifPresent(v -> v.validate(context.current())); + } + + public WorkflowState state() { + return state; + } + + public Object output() { + return toJavaValue(context.current()); + } + + public Object outputAsJsonNode() { + return context.current(); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java new file mode 100644 index 00000000..939fd1e8 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java @@ -0,0 +1,7 @@ +package io.serverlessworkflow.impl; + +public enum WorkflowState { + STARTED, + WAITING, + COMPLETED +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java index 787d17dc..47191272 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -19,21 +19,27 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.serverlessworkflow.api.WorkflowFormat; import io.serverlessworkflow.api.types.ExportAs; +import io.serverlessworkflow.api.types.FlowDirective; import io.serverlessworkflow.api.types.InputFrom; import io.serverlessworkflow.api.types.OutputAs; import io.serverlessworkflow.api.types.SchemaExternal; import io.serverlessworkflow.api.types.SchemaInline; import io.serverlessworkflow.api.types.SchemaUnion; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.impl.expressions.Expression; import io.serverlessworkflow.impl.expressions.ExpressionFactory; import io.serverlessworkflow.impl.expressions.ExpressionUtils; import io.serverlessworkflow.impl.json.JsonUtils; import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.resources.ResourceLoader; import io.serverlessworkflow.resources.StaticResource; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.Optional; @@ -46,14 +52,14 @@ public static Optional getSchemaValidator( return node.map(n -> validatorFactory.getValidator(n)); } - public static Optional schemaToNode(WorkflowFactories factories, SchemaUnion schema) { + public static Optional schemaToNode(ResourceLoader resourceLoader, SchemaUnion schema) { if (schema != null) { if (schema.getSchemaInline() != null) { SchemaInline inline = schema.getSchemaInline(); return Optional.of(JsonUtils.mapper().convertValue(inline.getDocument(), JsonNode.class)); } else if (schema.getSchemaExternal() != null) { SchemaExternal external = schema.getSchemaExternal(); - StaticResource resource = factories.getResourceLoader().loadStatic(external.getResource()); + StaticResource resource = resourceLoader.loadStatic(external.getResource()); ObjectMapper mapper = WorkflowFormat.fromFileName(resource.name()).mapper(); try (InputStream in = resource.open()) { return Optional.of(mapper.readTree(in)); @@ -89,9 +95,8 @@ public static Optional buildWorkflowFilter( private static WorkflowFilter buildWorkflowFilter( ExpressionFactory exprFactory, String str, Object object) { if (str != null) { - Expression expression = exprFactory.getExpression(str); - return expression::eval; - } else { + return buildWorkflowFilter(exprFactory, str); + } else if (object != null) { Object exprObj = ExpressionUtils.buildExpressionObject(object, exprFactory); return exprObj instanceof Map ? (w, t, n) -> @@ -99,5 +104,80 @@ private static WorkflowFilter buildWorkflowFilter( ExpressionUtils.evaluateExpressionMap((Map) exprObj, w, t, n)) : (w, t, n) -> JsonUtils.fromValue(object); } + throw new IllegalStateException("Both object and str are null"); + } + + private static TaskItem findTaskByName(ListIterator iter, String taskName) { + int currentIndex = iter.nextIndex(); + while (iter.hasPrevious()) { + TaskItem item = iter.previous(); + if (item.getName().equals(taskName)) { + return item; + } + } + while (iter.nextIndex() < currentIndex) { + iter.next(); + } + while (iter.hasNext()) { + TaskItem item = iter.next(); + if (item.getName().equals(taskName)) { + return item; + } + } + throw new IllegalArgumentException("Cannot find task with name " + taskName); + } + + public static void processTaskList(List tasks, WorkflowContext context) { + context.position().addProperty("do"); + if (!tasks.isEmpty()) { + ListIterator iter = tasks.listIterator(); + TaskItem nextTask = iter.next(); + while (nextTask != null) { + TaskItem task = nextTask; + context.position().addIndex(iter.nextIndex()).addProperty(task.getName()); + context + .definition() + .listeners() + .forEach(l -> l.onTaskStarted(context.position(), task.getTask())); + TaskContext taskContext = + context + .definition() + .taskExecutors() + .computeIfAbsent( + context.position().jsonPointer(), + k -> + context + .definition() + .taskFactory() + .getTaskExecutor(task.getTask(), context.definition())) + .apply(context, context.current()); + context.current(taskContext.output()); + FlowDirective flowDirective = taskContext.flowDirective(); + if (flowDirective.getFlowDirectiveEnum() != null) { + switch (flowDirective.getFlowDirectiveEnum()) { + case CONTINUE: + nextTask = iter.hasNext() ? iter.next() : null; + break; + case END: + case EXIT: + nextTask = null; + break; + } + } else { + nextTask = WorkflowUtils.findTaskByName(iter, flowDirective.getString()); + } + context + .definition() + .listeners() + .forEach(l -> l.onTaskEnded(context.position(), task.getTask())); + context.position().back(); + } + } + context.position().back(); + } + + public static WorkflowFilter buildWorkflowFilter(ExpressionFactory exprFactory, String str) { + Expression expression = exprFactory.getExpression(str); + return expression::eval; } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java index 3ed5d6e6..56e55120 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -24,7 +24,7 @@ import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; -import io.serverlessworkflow.impl.WorkflowFactories; +import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.WorkflowFilter; import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import java.util.Optional; @@ -40,53 +40,57 @@ public abstract class AbstractTaskExecutor implements TaskEx private Optional outputSchemaValidator = Optional.empty(); private Optional contextSchemaValidator = Optional.empty(); - protected AbstractTaskExecutor(T task, WorkflowFactories holder) { + protected AbstractTaskExecutor(T task, WorkflowDefinition definition) { this.task = task; - buildInputProcessors(holder); - buildOutputProcessors(holder); - buildContextProcessors(holder); + buildInputProcessors(definition); + buildOutputProcessors(definition); + buildContextProcessors(definition); } - private void buildInputProcessors(WorkflowFactories holder) { + private void buildInputProcessors(WorkflowDefinition definition) { if (task.getInput() != null) { Input input = task.getInput(); - this.inputProcessor = buildWorkflowFilter(holder.getExpressionFactory(), input.getFrom()); + this.inputProcessor = buildWorkflowFilter(definition.expressionFactory(), input.getFrom()); this.inputSchemaValidator = - getSchemaValidator(holder.getValidatorFactory(), schemaToNode(holder, input.getSchema())); + getSchemaValidator( + definition.validatorFactory(), + schemaToNode(definition.resourceLoader(), input.getSchema())); } } - private void buildOutputProcessors(WorkflowFactories holder) { + private void buildOutputProcessors(WorkflowDefinition definition) { if (task.getOutput() != null) { Output output = task.getOutput(); - this.outputProcessor = buildWorkflowFilter(holder.getExpressionFactory(), output.getAs()); + this.outputProcessor = buildWorkflowFilter(definition.expressionFactory(), output.getAs()); this.outputSchemaValidator = getSchemaValidator( - holder.getValidatorFactory(), schemaToNode(holder, output.getSchema())); + definition.validatorFactory(), + schemaToNode(definition.resourceLoader(), output.getSchema())); } } - private void buildContextProcessors(WorkflowFactories holder) { + private void buildContextProcessors(WorkflowDefinition definition) { if (task.getExport() != null) { Export export = task.getExport(); if (export.getAs() != null) { - this.contextProcessor = buildWorkflowFilter(holder.getExpressionFactory(), export.getAs()); + this.contextProcessor = buildWorkflowFilter(definition.expressionFactory(), export.getAs()); } this.contextSchemaValidator = getSchemaValidator( - holder.getValidatorFactory(), schemaToNode(holder, export.getSchema())); + definition.validatorFactory(), + schemaToNode(definition.resourceLoader(), export.getSchema())); } } @Override - public JsonNode apply(WorkflowContext workflowContext, JsonNode rawInput) { + public TaskContext apply(WorkflowContext workflowContext, JsonNode rawInput) { TaskContext taskContext = new TaskContext<>(rawInput, task); inputSchemaValidator.ifPresent(s -> s.validate(taskContext.rawInput())); inputProcessor.ifPresent( p -> taskContext.input( p.apply(workflowContext, Optional.of(taskContext), taskContext.rawInput()))); - taskContext.rawOutput(internalExecute(workflowContext, taskContext, taskContext.input())); + internalExecute(workflowContext, taskContext); outputProcessor.ifPresent( p -> taskContext.output( @@ -97,9 +101,8 @@ public JsonNode apply(WorkflowContext workflowContext, JsonNode rawInput) { workflowContext.context( p.apply(workflowContext, Optional.of(taskContext), workflowContext.context()))); contextSchemaValidator.ifPresent(s -> s.validate(workflowContext.context())); - return taskContext.output(); + return taskContext; } - protected abstract JsonNode internalExecute( - WorkflowContext workflow, TaskContext task, JsonNode node); + protected abstract void internalExecute(WorkflowContext workflow, TaskContext taskContext); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index cb76e395..117a8ed2 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -18,7 +18,7 @@ import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.impl.WorkflowFactories; +import io.serverlessworkflow.impl.WorkflowDefinition; public class DefaultTaskExecutorFactory implements TaskExecutorFactory { @@ -30,12 +30,19 @@ public static TaskExecutorFactory get() { protected DefaultTaskExecutorFactory() {} - public TaskExecutor getTaskExecutor(Task task, WorkflowFactories factories) { + public TaskExecutor getTaskExecutor( + Task task, WorkflowDefinition definition) { if (task.getCallTask() != null) { CallTask callTask = task.getCallTask(); if (callTask.getCallHTTP() != null) { - return new HttpExecutor(callTask.getCallHTTP(), factories); + return new HttpExecutor(callTask.getCallHTTP(), definition); } + } else if (task.getSwitchTask() != null) { + return new SwitchExecutor(task.getSwitchTask(), definition); + } else if (task.getDoTask() != null) { + return new DoExecutor(task.getDoTask(), definition); + } else if (task.getSetTask() != null) { + return new SetExecutor(task.getSetTask(), definition); } throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java new file mode 100644 index 00000000..30f35f95 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java @@ -0,0 +1,34 @@ +/* + * 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.executors; + +import io.serverlessworkflow.api.types.DoTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowUtils; + +public class DoExecutor extends AbstractTaskExecutor { + + protected DoExecutor(DoTask task, WorkflowDefinition definition) { + super(task, definition); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + WorkflowUtils.processTaskList(task.getDo(), workflow); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index e17fd8dc..b8548549 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -24,7 +24,7 @@ import io.serverlessworkflow.api.types.UriTemplate; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; -import io.serverlessworkflow.impl.WorkflowFactories; +import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.expressions.Expression; import io.serverlessworkflow.impl.expressions.ExpressionFactory; import io.serverlessworkflow.impl.expressions.ExpressionUtils; @@ -58,25 +58,25 @@ private interface RequestSupplier { JsonNode apply(Builder request, WorkflowContext workflow, TaskContext task, JsonNode node); } - public HttpExecutor(CallHTTP task, WorkflowFactories holder) { - super(task, holder); + public HttpExecutor(CallHTTP task, WorkflowDefinition definition) { + super(task, definition); HTTPArguments httpArgs = task.getWith(); - this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint(), holder.getExpressionFactory()); + this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint(), definition.expressionFactory()); this.headersMap = httpArgs.getHeaders() != null ? ExpressionUtils.buildExpressionMap( - httpArgs.getHeaders().getAdditionalProperties(), holder.getExpressionFactory()) + httpArgs.getHeaders().getAdditionalProperties(), definition.expressionFactory()) : Map.of(); this.queryMap = httpArgs.getQuery() != null ? ExpressionUtils.buildExpressionMap( - httpArgs.getQuery().getAdditionalProperties(), holder.getExpressionFactory()) + httpArgs.getQuery().getAdditionalProperties(), definition.expressionFactory()) : Map.of(); switch (httpArgs.getMethod().toUpperCase()) { case HttpMethod.POST: Object body = ExpressionUtils.buildExpressionObject( - httpArgs.getBody(), holder.getExpressionFactory()); + httpArgs.getBody(), definition.expressionFactory()); this.requestFunction = (request, workflow, context, node) -> request.post( @@ -92,8 +92,8 @@ public HttpExecutor(CallHTTP task, WorkflowFactories holder) { } @Override - protected JsonNode internalExecute( - WorkflowContext workflow, TaskContext taskContext, JsonNode input) { + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + JsonNode input = taskContext.input(); WebTarget target = targetSupplier.apply(workflow, taskContext, input); for (Entry entry : ExpressionUtils.evaluateExpressionMap(queryMap, workflow, Optional.of(taskContext), input) @@ -103,7 +103,7 @@ protected JsonNode internalExecute( Builder request = target.request(); ExpressionUtils.evaluateExpressionMap(headersMap, workflow, Optional.of(taskContext), input) .forEach(request::header); - return requestFunction.apply(request, workflow, taskContext, input); + taskContext.rawOutput(requestFunction.apply(request, workflow, taskContext, input)); } private static TargetSupplier getTargetSupplier( diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java new file mode 100644 index 00000000..b9d9db86 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java @@ -0,0 +1,39 @@ +/* + * 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.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.SetTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.json.JsonUtils; +import io.serverlessworkflow.impl.json.MergeUtils; + +public class SetExecutor extends AbstractTaskExecutor { + + private JsonNode toBeSet; + + protected SetExecutor(SetTask task, WorkflowDefinition definition) { + super(task, definition); + this.toBeSet = JsonUtils.fromValue(task.getSet().getAdditionalProperties()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + taskContext.rawOutput(MergeUtils.merge(toBeSet, taskContext.input())); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java new file mode 100644 index 00000000..9f421acb --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java @@ -0,0 +1,50 @@ +package io.serverlessworkflow.impl.executors; + +import io.serverlessworkflow.api.types.FlowDirective; +import io.serverlessworkflow.api.types.SwitchCase; +import io.serverlessworkflow.api.types.SwitchItem; +import io.serverlessworkflow.api.types.SwitchTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowUtils; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class SwitchExecutor extends AbstractTaskExecutor { + + private Map workflowFilters = new ConcurrentHashMap<>(); + private FlowDirective defaultDirective; + + protected SwitchExecutor(SwitchTask task, WorkflowDefinition definition) { + super(task, definition); + for (SwitchItem item : task.getSwitch()) { + SwitchCase switchCase = item.getSwitchCase(); + if (switchCase.getWhen() != null) { + workflowFilters.put( + switchCase, + WorkflowUtils.buildWorkflowFilter( + definition.expressionFactory(), switchCase.getWhen())); + } else { + defaultDirective = switchCase.getThen(); + } + } + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + for (Entry entry : workflowFilters.entrySet()) { + if (entry + .getValue() + .apply(workflow, Optional.of(taskContext), taskContext.input()) + .asBoolean()) { + taskContext.flowDirective(entry.getKey().getThen()); + return; + } + } + taskContext.flowDirective(defaultDirective); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java index 8c896385..62228199 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java @@ -17,8 +17,9 @@ import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import java.util.function.BiFunction; public interface TaskExecutor - extends BiFunction {} + extends BiFunction> {} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java index 85cef4b1..8c399cf6 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java @@ -17,8 +17,8 @@ import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.impl.WorkflowFactories; +import io.serverlessworkflow.impl.WorkflowDefinition; public interface TaskExecutorFactory { - TaskExecutor getTaskExecutor(Task task, WorkflowFactories factories); + TaskExecutor getTaskExecutor(Task task, WorkflowDefinition definition); } diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index c1dbd85f..187c0215 100644 --- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.stream.Stream; import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -30,15 +31,18 @@ public class WorkflowDefinitionTest { + private static WorkflowApplication appl; + + @BeforeAll + static void init() { + appl = WorkflowApplication.builder().build(); + } + @ParameterizedTest @MethodSource("provideParameters") void testWorkflowExecution(String fileName, Object input, Condition condition) throws IOException { - assertThat( - WorkflowDefinition.builder(readWorkflowFromClasspath(fileName)) - .build() - .execute(input) - .output()) + assertThat(appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(input).output()) .is(condition); } @@ -52,10 +56,7 @@ void testWrongSchema(String fileName) { IllegalArgumentException exception = catchThrowableOfType( IllegalArgumentException.class, - () -> - WorkflowDefinition.builder(readWorkflowFromClasspath(fileName)) - .build() - .execute(Map.of())); + () -> appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(Map.of())); assertThat(exception) .isNotNull() .hasMessageContaining("There are JsonSchema validation errors"); @@ -82,6 +83,39 @@ private static Stream provideParameters() { Arguments.of( "callPostHttp.yaml", Map.of("name", "Javierito", "status", "available"), - new Condition<>(o -> o.equals("Javierito"), "CallHttpPostCondition"))); + new Condition<>(o -> o.equals("Javierito"), "CallHttpPostCondition")), + Arguments.of( + "switch-then-string.yaml", + Map.of("orderType", "electronic"), + new Condition( + o -> + o.equals( + Map.of("orderType", "electronic", "validate", true, "status", "fulfilled")), + "switch-electronic")), + Arguments.of( + "switch-then-string.yaml", + Map.of("orderType", "physical"), + new Condition( + o -> + o.equals( + Map.of( + "orderType", + "physical", + "inventory", + "clear", + "items", + 1, + "address", + "Elmer St")), + "switch-physical")), + Arguments.of( + "switch-then-string.yaml", + Map.of("orderType", "unknown"), + new Condition( + o -> + o.equals( + Map.of( + "orderType", "unknown", "log", "warn", "message", "something's wrong")), + "switch-unknown"))); } } diff --git a/impl/src/test/resources/call-http-endpoint-interpolation.yaml b/impl/src/test/resources/call-http-endpoint-interpolation.yaml index 8380a9aa..4d6453a5 100644 --- a/impl/src/test/resources/call-http-endpoint-interpolation.yaml +++ b/impl/src/test/resources/call-http-endpoint-interpolation.yaml @@ -1,5 +1,5 @@ document: - dsl: '1.0.0-alpha5' + dsl: 1.0.0-alpha5 namespace: examples name: call-http-shorthand-endpoint version: '0.1.0' diff --git a/impl/src/test/resources/call-http-query-parameters-external-schema.yaml b/impl/src/test/resources/call-http-query-parameters-external-schema.yaml index a5bb1437..e72b79e4 100644 --- a/impl/src/test/resources/call-http-query-parameters-external-schema.yaml +++ b/impl/src/test/resources/call-http-query-parameters-external-schema.yaml @@ -1,7 +1,7 @@ document: dsl: 1.0.0-alpha2 namespace: examples - name: http-query-params + name: http-query-params-schema version: 1.0.0-alpha2 input: schema: diff --git a/impl/src/test/resources/callGetHttp.yaml b/impl/src/test/resources/callGetHttp.yaml index 0fdeb10a..eb7145fa 100644 --- a/impl/src/test/resources/callGetHttp.yaml +++ b/impl/src/test/resources/callGetHttp.yaml @@ -1,7 +1,7 @@ document: dsl: 1.0.0-alpha1 - namespace: default - name: http-call-with-response-output + namespace: examples + name: http-call-with-response version: 1.0.0 do: - getPet: diff --git a/impl/src/test/resources/callPostHttp.yaml b/impl/src/test/resources/callPostHttp.yaml index d898dbf7..0e998094 100644 --- a/impl/src/test/resources/callPostHttp.yaml +++ b/impl/src/test/resources/callPostHttp.yaml @@ -1,6 +1,6 @@ document: dsl: 1.0.0-alpha1 - namespace: default + namespace: examples name: http-call-with-response-output version: 1.0.0 do: diff --git a/impl/src/test/resources/switch-then-string.yaml b/impl/src/test/resources/switch-then-string.yaml new file mode 100644 index 00000000..6c54b359 --- /dev/null +++ b/impl/src/test/resources/switch-then-string.yaml @@ -0,0 +1,45 @@ +document: + dsl: 1.0.0-alpha5 + namespace: examples + name: switch + version: 0.1.0 +do: + - processOrder: + switch: + - case1: + when: .orderType == "electronic" + then: processElectronicOrder + - case2: + when: .orderType == "physical" + then: processPhysicalOrder + - default: + then: handleUnknownOrderType + - processElectronicOrder: + do: + - validatePayment: + set: + validate: true + - fulfillOrder: + set: + status: fulfilled + then: exit + - processPhysicalOrder: + do: + - checkInventory: + set: + inventory: clear + - packItems: + set: + items: 1 + - scheduleShipping: + set: + address: Elmer St + then: exit + - handleUnknownOrderType: + do: + - logWarning: + set: + log: warn + - notifyAdmin: + set: + message: something's wrong diff --git a/pom.xml b/pom.xml index 4f74b387..36fc113b 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ - 11 + 17 ${java.version} ${java.version} UTF-8 From e00292e4ca8c86a767b803b8998af51df529bf8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:45:24 +0000 Subject: [PATCH 09/22] Bump com.networknt:json-schema-validator from 1.5.3 to 1.5.4 Bumps [com.networknt:json-schema-validator](https://github.com/networknt/json-schema-validator) from 1.5.3 to 1.5.4. - [Release notes](https://github.com/networknt/json-schema-validator/releases) - [Changelog](https://github.com/networknt/json-schema-validator/blob/master/CHANGELOG.md) - [Commits](https://github.com/networknt/json-schema-validator/compare/1.5.3...1.5.4) --- updated-dependencies: - dependency-name: com.networknt:json-schema-validator dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 36fc113b..d2c39f73 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ 1.5.12 2.18.1 - 1.5.3 + 1.5.4 3.1.0 1.5.2 3.26.3 From fa3d1d3ef4cb832f0419070febd9ddac1e6844ed Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Mon, 25 Nov 2024 14:52:03 +0100 Subject: [PATCH 10/22] [Fix #473] Refactor project structure Signed-off-by: Francisco Javier Tirado Sarti --- api/pom.xml | 71 ------------ impl/bom/pom.xml | 20 ++++ impl/core/.checkstyle | 14 +++ impl/core/pom.xml | 48 ++++++++ .../impl/DefaultWorkflowPosition.java | 0 .../serverlessworkflow/impl/TaskContext.java | 0 .../impl/WorkflowApplication.java | 0 .../impl/WorkflowContext.java | 0 .../impl/WorkflowDefinition.java | 0 .../impl/WorkflowExecutionListener.java | 0 .../impl/WorkflowFilter.java | 0 .../impl/WorkflowInstance.java | 0 .../impl/WorkflowPosition.java | 0 .../impl/WorkflowState.java | 22 ++++ .../impl/WorkflowUtils.java | 0 .../impl/executors/AbstractTaskExecutor.java | 0 .../impl/executors/CallTaskExecutor.java | 37 +++++++ .../impl/executors/CallableTask.java | 30 +++++ .../executors/DefaultTaskExecutorFactory.java | 19 +++- .../impl/executors/DoExecutor.java | 0 .../impl/executors/SetExecutor.java | 0 .../impl/executors/SwitchExecutor.java | 15 +++ .../impl/executors/TaskExecutor.java | 0 .../impl/executors/TaskExecutorFactory.java | 0 .../impl/expressions/Expression.java | 0 .../impl/expressions/ExpressionFactory.java | 0 .../impl/expressions/ExpressionUtils.java | 0 .../ExpressionValidationException.java | 29 +++++ .../impl/expressions/JQExpression.java | 0 .../impl/expressions/JQExpressionFactory.java | 0 .../impl/expressions/ProxyMap.java | 0 .../impl/json/JsonUtils.java | 0 .../impl/json/MergeUtils.java | 0 .../jsonschema/DefaultSchemaValidator.java | 25 ++--- .../DefaultSchemaValidatorFactory.java | 0 .../impl/jsonschema/SchemaValidator.java | 0 .../jsonschema/SchemaValidatorFactory.java | 0 .../resources/ClasspathResource.java | 0 .../resources/DefaultResourceLoader.java | 0 .../DefaultResourceLoaderFactory.java | 34 ++++++ .../resources/DynamicResource.java | 0 .../resources/FileResource.java | 0 .../resources/HttpResource.java | 0 .../resources/ResourceLoader.java | 0 .../resources/ResourceLoaderFactory.java | 0 .../resources/StaticResource.java | 0 .../impl/WorkflowDefinitionTest.java | 87 +++++++++++++++ .../test/resources/switch-then-string.yaml | 0 impl/http/.checkstyle | 14 +++ impl/http/pom.xml | 43 ++++++++ .../impl/executors/HttpExecutor.java | 26 +++-- ...erlessworkflow.impl.executors.CallableTask | 1 + .../impl/HTTPWorkflowDefinitionTest.java} | 41 +------ .../call-http-endpoint-interpolation.yaml | 0 ...http-query-parameters-external-schema.yaml | 0 .../resources/call-http-query-parameters.yaml | 0 .../src/test/resources/callGetHttp.yaml | 0 .../src/test/resources/callPostHttp.yaml | 0 .../test/resources/schema/searchquery.yaml | 0 impl/pom.xml | 103 ++++++------------ .../impl/WorkflowState.java | 7 -- .../ExpressionValidationException.java | 14 --- .../DefaultResourceLoaderFactory.java | 19 ---- pom.xml | 61 +++++++++++ 64 files changed, 535 insertions(+), 245 deletions(-) create mode 100644 impl/bom/pom.xml create mode 100644 impl/core/.checkstyle create mode 100644 impl/core/pom.xml rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/TaskContext.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java (100%) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowState.java rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java (100%) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallTaskExecutor.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallableTask.java rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java (71%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java (74%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java (100%) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java (61%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java (100%) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/DynamicResource.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/FileResource.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/HttpResource.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java (100%) rename impl/{ => core}/src/main/java/io/serverlessworkflow/resources/StaticResource.java (100%) create mode 100644 impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java rename impl/{ => core}/src/test/resources/switch-then-string.yaml (100%) create mode 100644 impl/http/.checkstyle create mode 100644 impl/http/pom.xml rename impl/{ => http}/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java (89%) create mode 100644 impl/http/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask rename impl/{src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java => http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java} (72%) rename impl/{ => http}/src/test/resources/call-http-endpoint-interpolation.yaml (100%) rename impl/{ => http}/src/test/resources/call-http-query-parameters-external-schema.yaml (100%) rename impl/{ => http}/src/test/resources/call-http-query-parameters.yaml (100%) rename impl/{ => http}/src/test/resources/callGetHttp.yaml (100%) rename impl/{ => http}/src/test/resources/callPostHttp.yaml (100%) rename impl/{ => http}/src/test/resources/schema/searchquery.yaml (100%) delete mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java delete mode 100644 impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java delete mode 100644 impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java diff --git a/api/pom.xml b/api/pom.xml index d5128d57..f6186156 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -121,77 +121,6 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - - - - - - - - - - - ${project.build.directory}/checkstyle.log - true - true - true - false - false - ${checkstyle.logViolationsToConsole} - ${checkstyle.failOnViolation} - - ${project.build.sourceDirectory} - ${project.build.testSourceDirectory} - - - - - compile - - check - - - - - - com.spotify.fmt - fmt-maven-plugin - - src/main/java - src/test/java - false - .*\.java - false - false - - - - - - format - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - test-jar - - - - diff --git a/impl/bom/pom.xml b/impl/bom/pom.xml new file mode 100644 index 00000000..63ef0fe3 --- /dev/null +++ b/impl/bom/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-impl + 7.0.0-SNAPSHOT + + serverlessworkflow-impl-bom + pom + + + io.serverlessworkflow + serverlessworkflow-impl-core + + + io.serverlessworkflow + serverlessworkflow-impl-http + + + \ No newline at end of file diff --git a/impl/core/.checkstyle b/impl/core/.checkstyle new file mode 100644 index 00000000..cdd4188c --- /dev/null +++ b/impl/core/.checkstyle @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/impl/core/pom.xml b/impl/core/pom.xml new file mode 100644 index 00000000..597b3758 --- /dev/null +++ b/impl/core/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-impl + 7.0.0-SNAPSHOT + + serverlessworkflow-impl-core + + 1.1.0 + + + + io.serverlessworkflow + serverlessworkflow-api + 7.0.0-SNAPSHOT + + + com.networknt + json-schema-validator + + + net.thisptr + jackson-jq + ${version.net.thisptr} + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + diff --git a/impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowState.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowState.java new file mode 100644 index 00000000..310dbd0b --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowState.java @@ -0,0 +1,22 @@ +/* + * 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; + +public enum WorkflowState { + STARTED, + WAITING, + COMPLETED +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallTaskExecutor.java new file mode 100644 index 00000000..535057fa --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallTaskExecutor.java @@ -0,0 +1,37 @@ +/* + * 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.executors; + +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; + +public class CallTaskExecutor extends AbstractTaskExecutor { + + private final CallableTask callable; + + protected CallTaskExecutor(T task, WorkflowDefinition definition, CallableTask callable) { + super(task, definition); + this.callable = callable; + callable.init(task, definition); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + taskContext.rawOutput(callable.apply(workflow, taskContext, taskContext.input())); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallableTask.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallableTask.java new file mode 100644 index 00000000..ffb94912 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallableTask.java @@ -0,0 +1,30 @@ +/* + * 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.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; + +public interface CallableTask { + void init(T task, WorkflowDefinition definition); + + JsonNode apply(WorkflowContext workflowContext, TaskContext taskContext, JsonNode input); + + boolean accept(Class clazz); +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java similarity index 71% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index 117a8ed2..8ca2feb4 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -15,10 +15,13 @@ */ package io.serverlessworkflow.impl.executors; +import io.serverlessworkflow.api.types.CallHTTP; import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.impl.WorkflowDefinition; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; public class DefaultTaskExecutorFactory implements TaskExecutorFactory { @@ -30,12 +33,15 @@ public static TaskExecutorFactory get() { protected DefaultTaskExecutorFactory() {} + private ServiceLoader callTasks = ServiceLoader.load(CallableTask.class); + public TaskExecutor getTaskExecutor( Task task, WorkflowDefinition definition) { if (task.getCallTask() != null) { CallTask callTask = task.getCallTask(); if (callTask.getCallHTTP() != null) { - return new HttpExecutor(callTask.getCallHTTP(), definition); + return new CallTaskExecutor<>( + callTask.getCallHTTP(), definition, findCallTask(CallHTTP.class)); } } else if (task.getSwitchTask() != null) { return new SwitchExecutor(task.getSwitchTask(), definition); @@ -46,4 +52,15 @@ public TaskExecutor getTaskExecutor( } throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); } + + @SuppressWarnings("unchecked") + private CallableTask findCallTask(Class clazz) { + return (CallableTask) + callTasks.stream() + .map(Provider::get) + .filter(s -> s.accept(clazz)) + .findAny() + .orElseThrow( + () -> new UnsupportedOperationException(clazz.getName() + " not supported yet")); + } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java similarity index 74% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java index 9f421acb..f081dfa4 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java @@ -1,3 +1,18 @@ +/* + * 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.executors; import io.serverlessworkflow.api.types.FlowDirective; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java new file mode 100644 index 00000000..8dc1b4f2 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java @@ -0,0 +1,29 @@ +/* + * 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.expressions; + +public class ExpressionValidationException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExpressionValidationException(String message) { + super(message); + } + + public ExpressionValidationException(String message, Throwable ex) { + super(message, ex); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java similarity index 61% rename from impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java index 232926b3..d3ab3190 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java @@ -1,20 +1,17 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at + * Copyright 2020-Present The Serverless Workflow Specification Authors * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 * - * 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. + * 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.jsonschema; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java diff --git a/impl/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java b/impl/core/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java diff --git a/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java b/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java b/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java new file mode 100644 index 00000000..4efddb30 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java @@ -0,0 +1,34 @@ +/* + * 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.resources; + +import java.nio.file.Path; + +public class DefaultResourceLoaderFactory implements ResourceLoaderFactory { + + public static final ResourceLoaderFactory get() { + return factory; + } + + private static final ResourceLoaderFactory factory = new DefaultResourceLoaderFactory(); + + private DefaultResourceLoaderFactory() {} + + @Override + public ResourceLoader getResourceLoader(Path path) { + return new DefaultResourceLoader(path); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/DynamicResource.java b/impl/core/src/main/java/io/serverlessworkflow/resources/DynamicResource.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/DynamicResource.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/DynamicResource.java diff --git a/impl/src/main/java/io/serverlessworkflow/resources/FileResource.java b/impl/core/src/main/java/io/serverlessworkflow/resources/FileResource.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/FileResource.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/FileResource.java diff --git a/impl/src/main/java/io/serverlessworkflow/resources/HttpResource.java b/impl/core/src/main/java/io/serverlessworkflow/resources/HttpResource.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/HttpResource.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/HttpResource.java diff --git a/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java b/impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java diff --git a/impl/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java b/impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java diff --git a/impl/src/main/java/io/serverlessworkflow/resources/StaticResource.java b/impl/core/src/main/java/io/serverlessworkflow/resources/StaticResource.java similarity index 100% rename from impl/src/main/java/io/serverlessworkflow/resources/StaticResource.java rename to impl/core/src/main/java/io/serverlessworkflow/resources/StaticResource.java diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java new file mode 100644 index 00000000..f6c3455a --- /dev/null +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -0,0 +1,87 @@ +/* + * 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; + +import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.Map; +import java.util.stream.Stream; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class WorkflowDefinitionTest { + + private static WorkflowApplication appl; + + @BeforeAll + static void init() { + appl = WorkflowApplication.builder().build(); + } + + @ParameterizedTest + @MethodSource("provideParameters") + void testWorkflowExecution(String fileName, Object input, Condition condition) + throws IOException { + assertThat(appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(input).output()) + .is(condition); + } + + private static Stream provideParameters() { + Map petInput = Map.of("petId", 10); + Condition petCondition = + new Condition<>( + o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"); + return Stream.of( + Arguments.of( + "switch-then-string.yaml", + Map.of("orderType", "electronic"), + new Condition( + o -> + o.equals( + Map.of("orderType", "electronic", "validate", true, "status", "fulfilled")), + "switch-electronic")), + Arguments.of( + "switch-then-string.yaml", + Map.of("orderType", "physical"), + new Condition( + o -> + o.equals( + Map.of( + "orderType", + "physical", + "inventory", + "clear", + "items", + 1, + "address", + "Elmer St")), + "switch-physical")), + Arguments.of( + "switch-then-string.yaml", + Map.of("orderType", "unknown"), + new Condition( + o -> + o.equals( + Map.of( + "orderType", "unknown", "log", "warn", "message", "something's wrong")), + "switch-unknown"))); + } +} diff --git a/impl/src/test/resources/switch-then-string.yaml b/impl/core/src/test/resources/switch-then-string.yaml similarity index 100% rename from impl/src/test/resources/switch-then-string.yaml rename to impl/core/src/test/resources/switch-then-string.yaml diff --git a/impl/http/.checkstyle b/impl/http/.checkstyle new file mode 100644 index 00000000..a33f7d91 --- /dev/null +++ b/impl/http/.checkstyle @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/impl/http/pom.xml b/impl/http/pom.xml new file mode 100644 index 00000000..1b19e5a4 --- /dev/null +++ b/impl/http/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-impl + 7.0.0-SNAPSHOT + + serverlessworkflow-impl-http + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.media + jersey-media-json-jackson + + + io.serverlessworkflow + serverlessworkflow-impl-core + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + \ No newline at end of file diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java similarity index 89% rename from impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java rename to impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index b8548549..684acd47 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -21,6 +21,7 @@ import io.serverlessworkflow.api.types.Endpoint; import io.serverlessworkflow.api.types.EndpointUri; import io.serverlessworkflow.api.types.HTTPArguments; +import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.api.types.UriTemplate; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; @@ -39,14 +40,14 @@ import java.util.Map.Entry; import java.util.Optional; -public class HttpExecutor extends AbstractTaskExecutor { +public class HttpExecutor implements CallableTask { private static final Client client = ClientBuilder.newClient(); - private final TargetSupplier targetSupplier; - private final Map headersMap; - private final Map queryMap; - private final RequestSupplier requestFunction; + private TargetSupplier targetSupplier; + private Map headersMap; + private Map queryMap; + private RequestSupplier requestFunction; @FunctionalInterface private interface TargetSupplier { @@ -58,8 +59,8 @@ private interface RequestSupplier { JsonNode apply(Builder request, WorkflowContext workflow, TaskContext task, JsonNode node); } - public HttpExecutor(CallHTTP task, WorkflowDefinition definition) { - super(task, definition); + @Override + public void init(CallHTTP task, WorkflowDefinition definition) { HTTPArguments httpArgs = task.getWith(); this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint(), definition.expressionFactory()); this.headersMap = @@ -92,8 +93,8 @@ public HttpExecutor(CallHTTP task, WorkflowDefinition definition) { } @Override - protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { - JsonNode input = taskContext.input(); + public JsonNode apply( + WorkflowContext workflow, TaskContext taskContext, JsonNode input) { WebTarget target = targetSupplier.apply(workflow, taskContext, input); for (Entry entry : ExpressionUtils.evaluateExpressionMap(queryMap, workflow, Optional.of(taskContext), input) @@ -103,7 +104,12 @@ protected void internalExecute(WorkflowContext workflow, TaskContext t Builder request = target.request(); ExpressionUtils.evaluateExpressionMap(headersMap, workflow, Optional.of(taskContext), input) .forEach(request::header); - taskContext.rawOutput(requestFunction.apply(request, workflow, taskContext, input)); + return requestFunction.apply(request, workflow, taskContext, input); + } + + @Override + public boolean accept(Class clazz) { + return clazz.equals(CallHTTP.class); } private static TargetSupplier getTargetSupplier( diff --git a/impl/http/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask b/impl/http/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask new file mode 100644 index 00000000..7d5e6bf9 --- /dev/null +++ b/impl/http/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask @@ -0,0 +1 @@ +io.serverlessworkflow.impl.executors.HttpExecutor \ No newline at end of file diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java similarity index 72% rename from impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java rename to impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java index 187c0215..dacdfe2e 100644 --- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java @@ -1,5 +1,3 @@ -package io.serverlessworkflow.impl; - /* * Copyright 2020-Present The Serverless Workflow Specification Authors * @@ -15,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.serverlessworkflow.impl; + import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowableOfType; @@ -29,7 +29,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -public class WorkflowDefinitionTest { +public class HTTPWorkflowDefinitionTest { private static WorkflowApplication appl; @@ -83,39 +83,6 @@ private static Stream provideParameters() { Arguments.of( "callPostHttp.yaml", Map.of("name", "Javierito", "status", "available"), - new Condition<>(o -> o.equals("Javierito"), "CallHttpPostCondition")), - Arguments.of( - "switch-then-string.yaml", - Map.of("orderType", "electronic"), - new Condition( - o -> - o.equals( - Map.of("orderType", "electronic", "validate", true, "status", "fulfilled")), - "switch-electronic")), - Arguments.of( - "switch-then-string.yaml", - Map.of("orderType", "physical"), - new Condition( - o -> - o.equals( - Map.of( - "orderType", - "physical", - "inventory", - "clear", - "items", - 1, - "address", - "Elmer St")), - "switch-physical")), - Arguments.of( - "switch-then-string.yaml", - Map.of("orderType", "unknown"), - new Condition( - o -> - o.equals( - Map.of( - "orderType", "unknown", "log", "warn", "message", "something's wrong")), - "switch-unknown"))); + new Condition<>(o -> o.equals("Javierito"), "CallHttpPostCondition"))); } } diff --git a/impl/src/test/resources/call-http-endpoint-interpolation.yaml b/impl/http/src/test/resources/call-http-endpoint-interpolation.yaml similarity index 100% rename from impl/src/test/resources/call-http-endpoint-interpolation.yaml rename to impl/http/src/test/resources/call-http-endpoint-interpolation.yaml diff --git a/impl/src/test/resources/call-http-query-parameters-external-schema.yaml b/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml similarity index 100% rename from impl/src/test/resources/call-http-query-parameters-external-schema.yaml rename to impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml diff --git a/impl/src/test/resources/call-http-query-parameters.yaml b/impl/http/src/test/resources/call-http-query-parameters.yaml similarity index 100% rename from impl/src/test/resources/call-http-query-parameters.yaml rename to impl/http/src/test/resources/call-http-query-parameters.yaml diff --git a/impl/src/test/resources/callGetHttp.yaml b/impl/http/src/test/resources/callGetHttp.yaml similarity index 100% rename from impl/src/test/resources/callGetHttp.yaml rename to impl/http/src/test/resources/callGetHttp.yaml diff --git a/impl/src/test/resources/callPostHttp.yaml b/impl/http/src/test/resources/callPostHttp.yaml similarity index 100% rename from impl/src/test/resources/callPostHttp.yaml rename to impl/http/src/test/resources/callPostHttp.yaml diff --git a/impl/src/test/resources/schema/searchquery.yaml b/impl/http/src/test/resources/schema/searchquery.yaml similarity index 100% rename from impl/src/test/resources/schema/searchquery.yaml rename to impl/http/src/test/resources/schema/searchquery.yaml diff --git a/impl/pom.xml b/impl/pom.xml index 770f8090..191dc39d 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -6,78 +6,37 @@ 7.0.0-SNAPSHOT serverlessworkflow-impl + pom - 3.1.9 - 1.1.0 + 3.1.9 - - - io.serverlessworkflow - serverlessworkflow-api - 7.0.0-SNAPSHOT - - - org.glassfish.jersey.core - jersey-client - ${version.org.glassfish.jersey} - - - org.glassfish.jersey.media - jersey-media-json-jackson - ${version.org.glassfish.jersey} - - - com.networknt - json-schema-validator - - - net.thisptr - jackson-jq - ${version.net.thisptr} - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-params - test - - - org.assertj - assertj-core - test - - - - - - com.spotify.fmt - fmt-maven-plugin - - src/main/java - src/test/java - false - .*\.java - false - false - - - - - - format - - - - - - + + + + io.serverlessworkflow + serverlessworkflow-impl-core + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-impl-http + ${project.version} + + + org.glassfish.jersey.core + jersey-client + ${version.org.glassfish.jersey} + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${version.org.glassfish.jersey} + + + + + http + core + bom + \ No newline at end of file diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java deleted file mode 100644 index 939fd1e8..00000000 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowState.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.serverlessworkflow.impl; - -public enum WorkflowState { - STARTED, - WAITING, - COMPLETED -} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java deleted file mode 100644 index 16fe144f..00000000 --- a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.serverlessworkflow.impl.expressions; - -public class ExpressionValidationException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public ExpressionValidationException(String message) { - super(message); - } - - public ExpressionValidationException(String message, Throwable ex) { - super(message, ex); - } -} diff --git a/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java b/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java deleted file mode 100644 index 5a33601b..00000000 --- a/impl/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.serverlessworkflow.resources; - -import java.nio.file.Path; - -public class DefaultResourceLoaderFactory implements ResourceLoaderFactory { - - public static final ResourceLoaderFactory get() { - return factory; - } - - private static final ResourceLoaderFactory factory = new DefaultResourceLoaderFactory(); - - private DefaultResourceLoaderFactory() {} - - @Override - public ResourceLoader getResourceLoader(Path path) { - return new DefaultResourceLoader(path); - } -} diff --git a/pom.xml b/pom.xml index 36fc113b..da0a7678 100644 --- a/pom.xml +++ b/pom.xml @@ -232,6 +232,67 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + + + + + + + + + + + + + ${project.build.directory}/checkstyle.log + true + true + true + false + false + ${checkstyle.logViolationsToConsole} + ${checkstyle.failOnViolation} + + ${project.build.sourceDirectory} + ${project.build.testSourceDirectory} + + + + + compile + + check + + + + + + com.spotify.fmt + fmt-maven-plugin + + src/main/java + src/test/java + false + .*\.java + false + false + + + + + + format + + + + + From 4db3a21aef2ce98b3e5d2deba819f7591b2b5172 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Mon, 25 Nov 2024 19:50:15 +0100 Subject: [PATCH 11/22] Enable callable task in TaskExecutorFactory Signed-off-by: Francisco Javier Tirado Sarti --- .../executors/DefaultTaskExecutorFactory.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index 8ca2feb4..b5ac0119 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -15,7 +15,11 @@ */ package io.serverlessworkflow.impl.executors; +import io.serverlessworkflow.api.types.CallAsyncAPI; +import io.serverlessworkflow.api.types.CallFunction; +import io.serverlessworkflow.api.types.CallGRPC; import io.serverlessworkflow.api.types.CallHTTP; +import io.serverlessworkflow.api.types.CallOpenAPI; import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; @@ -42,6 +46,18 @@ public TaskExecutor getTaskExecutor( if (callTask.getCallHTTP() != null) { return new CallTaskExecutor<>( callTask.getCallHTTP(), definition, findCallTask(CallHTTP.class)); + } else if (callTask.getCallAsyncAPI() != null) { + return new CallTaskExecutor<>( + callTask.getCallAsyncAPI(), definition, findCallTask(CallAsyncAPI.class)); + } else if (callTask.getCallGRPC() != null) { + return new CallTaskExecutor<>( + callTask.getCallGRPC(), definition, findCallTask(CallGRPC.class)); + } else if (callTask.getCallOpenAPI() != null) { + return new CallTaskExecutor<>( + callTask.getCallOpenAPI(), definition, findCallTask(CallOpenAPI.class)); + } else if (callTask.getCallFunction() != null) { + return new CallTaskExecutor<>( + callTask.getCallFunction(), definition, findCallTask(CallFunction.class)); } } else if (task.getSwitchTask() != null) { return new SwitchExecutor(task.getSwitchTask(), definition); From 50a6a33b646086eb4a5fb82efd46b2b1f47ac17b Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:23:28 +0100 Subject: [PATCH 12/22] Migration to v4 actions (#479) Signed-off-by: Francisco Javier Tirado Sarti --- .github/workflows/maven-verify.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 12ab91f8..a9f5077c 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -14,10 +14,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99b13727..2d002124 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: github-token: ${{secrets.GITHUB_TOKEN}} metadata-file-path: '.github/project.yml' - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Import GPG key id: import_gpg @@ -30,7 +30,7 @@ jobs: passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 From 7174e2e67f39f15c01df5dfeafbae0996a31012b Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Tue, 26 Nov 2024 12:45:31 +0100 Subject: [PATCH 13/22] Change resource package location Signed-off-by: Francisco Javier Tirado Sarti --- .../java/io/serverlessworkflow/impl/WorkflowApplication.java | 5 +++-- .../java/io/serverlessworkflow/impl/WorkflowDefinition.java | 3 ++- .../main/java/io/serverlessworkflow/impl/WorkflowUtils.java | 5 +++-- .../{ => impl}/resources/ClasspathResource.java | 2 +- .../{ => impl}/resources/DefaultResourceLoader.java | 2 +- .../{ => impl}/resources/DefaultResourceLoaderFactory.java | 2 +- .../{ => impl}/resources/DynamicResource.java | 2 +- .../{ => impl}/resources/FileResource.java | 2 +- .../{ => impl}/resources/HttpResource.java | 2 +- .../{ => impl}/resources/ResourceLoader.java | 2 +- .../{ => impl}/resources/ResourceLoaderFactory.java | 2 +- .../{ => impl}/resources/StaticResource.java | 2 +- impl/core/src/test/resources/switch-then-string.yaml | 2 +- .../src/test/resources/call-http-endpoint-interpolation.yaml | 2 +- .../call-http-query-parameters-external-schema.yaml | 2 +- impl/http/src/test/resources/call-http-query-parameters.yaml | 2 +- impl/http/src/test/resources/callGetHttp.yaml | 2 +- impl/http/src/test/resources/callPostHttp.yaml | 2 +- 18 files changed, 23 insertions(+), 20 deletions(-) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/ClasspathResource.java (95%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/DefaultResourceLoader.java (98%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/DefaultResourceLoaderFactory.java (95%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/DynamicResource.java (95%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/FileResource.java (96%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/HttpResource.java (96%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/ResourceLoader.java (95%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/ResourceLoaderFactory.java (94%) rename impl/core/src/main/java/io/serverlessworkflow/{ => impl}/resources/StaticResource.java (94%) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java index 3fd81b00..02d807f3 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java @@ -23,8 +23,9 @@ import io.serverlessworkflow.impl.expressions.JQExpressionFactory; import io.serverlessworkflow.impl.jsonschema.DefaultSchemaValidatorFactory; import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; -import io.serverlessworkflow.resources.DefaultResourceLoaderFactory; -import io.serverlessworkflow.resources.ResourceLoaderFactory; +import io.serverlessworkflow.impl.resources.DefaultResourceLoaderFactory; +import io.serverlessworkflow.impl.resources.ResourceLoaderFactory; + import java.util.Collection; import java.util.Collections; import java.util.HashSet; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index 136d2853..f593cea3 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -27,7 +27,8 @@ import io.serverlessworkflow.impl.json.JsonUtils; import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; -import io.serverlessworkflow.resources.ResourceLoader; +import io.serverlessworkflow.impl.resources.ResourceLoader; + import java.nio.file.Path; import java.util.Collection; import java.util.Map; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java index 47191272..259a0e6a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -33,8 +33,9 @@ import io.serverlessworkflow.impl.json.JsonUtils; import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; -import io.serverlessworkflow.resources.ResourceLoader; -import io.serverlessworkflow.resources.StaticResource; +import io.serverlessworkflow.impl.resources.ResourceLoader; +import io.serverlessworkflow.impl.resources.StaticResource; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ClasspathResource.java similarity index 95% rename from impl/core/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/ClasspathResource.java index 81455712..dad39237 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/ClasspathResource.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ClasspathResource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import java.io.InputStream; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoader.java similarity index 98% rename from impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoader.java index be6015b8..a23a9224 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoader.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import io.serverlessworkflow.api.types.Endpoint; import io.serverlessworkflow.api.types.EndpointUri; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoaderFactory.java similarity index 95% rename from impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoaderFactory.java index 4efddb30..60717e1d 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/DefaultResourceLoaderFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoaderFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import java.nio.file.Path; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/DynamicResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DynamicResource.java similarity index 95% rename from impl/core/src/main/java/io/serverlessworkflow/resources/DynamicResource.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/DynamicResource.java index 476f24e7..ee9432c0 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/DynamicResource.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DynamicResource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.impl.TaskContext; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/FileResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/FileResource.java similarity index 96% rename from impl/core/src/main/java/io/serverlessworkflow/resources/FileResource.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/FileResource.java index a8b54ff7..6358b6ce 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/FileResource.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/FileResource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import java.io.IOException; import java.io.InputStream; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/HttpResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/HttpResource.java similarity index 96% rename from impl/core/src/main/java/io/serverlessworkflow/resources/HttpResource.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/HttpResource.java index 27d64a94..906e5c83 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/HttpResource.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/HttpResource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import java.io.IOException; import java.io.InputStream; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java similarity index 95% rename from impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java index 1e6ff2d5..346856bd 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoader.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import io.serverlessworkflow.api.types.ExternalResource; import io.serverlessworkflow.impl.WorkflowContext; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoaderFactory.java similarity index 94% rename from impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoaderFactory.java index 56347041..c064672b 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/ResourceLoaderFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoaderFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import java.nio.file.Path; diff --git a/impl/core/src/main/java/io/serverlessworkflow/resources/StaticResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/StaticResource.java similarity index 94% rename from impl/core/src/main/java/io/serverlessworkflow/resources/StaticResource.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/resources/StaticResource.java index 3b52a79c..32443dfc 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/resources/StaticResource.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/StaticResource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.resources; +package io.serverlessworkflow.impl.resources; import java.io.InputStream; diff --git a/impl/core/src/test/resources/switch-then-string.yaml b/impl/core/src/test/resources/switch-then-string.yaml index 6c54b359..a35ebd45 100644 --- a/impl/core/src/test/resources/switch-then-string.yaml +++ b/impl/core/src/test/resources/switch-then-string.yaml @@ -1,6 +1,6 @@ document: dsl: 1.0.0-alpha5 - namespace: examples + namespace: test name: switch version: 0.1.0 do: diff --git a/impl/http/src/test/resources/call-http-endpoint-interpolation.yaml b/impl/http/src/test/resources/call-http-endpoint-interpolation.yaml index 4d6453a5..5c1239f0 100644 --- a/impl/http/src/test/resources/call-http-endpoint-interpolation.yaml +++ b/impl/http/src/test/resources/call-http-endpoint-interpolation.yaml @@ -1,6 +1,6 @@ document: dsl: 1.0.0-alpha5 - namespace: examples + namespace: test name: call-http-shorthand-endpoint version: '0.1.0' do: diff --git a/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml b/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml index e72b79e4..9488592e 100644 --- a/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml +++ b/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml @@ -1,6 +1,6 @@ document: dsl: 1.0.0-alpha2 - namespace: examples + namespace: test name: http-query-params-schema version: 1.0.0-alpha2 input: diff --git a/impl/http/src/test/resources/call-http-query-parameters.yaml b/impl/http/src/test/resources/call-http-query-parameters.yaml index 75f33378..d209bf07 100644 --- a/impl/http/src/test/resources/call-http-query-parameters.yaml +++ b/impl/http/src/test/resources/call-http-query-parameters.yaml @@ -1,6 +1,6 @@ document: dsl: 1.0.0-alpha2 - namespace: examples + namespace: test name: http-query-params version: 1.0.0-alpha2 input: diff --git a/impl/http/src/test/resources/callGetHttp.yaml b/impl/http/src/test/resources/callGetHttp.yaml index eb7145fa..6fc07807 100644 --- a/impl/http/src/test/resources/callGetHttp.yaml +++ b/impl/http/src/test/resources/callGetHttp.yaml @@ -1,6 +1,6 @@ document: dsl: 1.0.0-alpha1 - namespace: examples + namespace: test name: http-call-with-response version: 1.0.0 do: diff --git a/impl/http/src/test/resources/callPostHttp.yaml b/impl/http/src/test/resources/callPostHttp.yaml index 0e998094..d66dcfaa 100644 --- a/impl/http/src/test/resources/callPostHttp.yaml +++ b/impl/http/src/test/resources/callPostHttp.yaml @@ -1,6 +1,6 @@ document: dsl: 1.0.0-alpha1 - namespace: examples + namespace: test name: http-call-with-response-output version: 1.0.0 do: From 399ad9d1be89a767d8eddf2f2a7a022adbf9f426 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Mon, 25 Nov 2024 21:39:15 +0100 Subject: [PATCH 14/22] [Fix_#474] For task implementation Signed-off-by: Francisco Javier Tirado Sarti --- .../serverlessworkflow/impl/ContextAware.java | 22 +++++++ .../impl/DefaultWorkflowPosition.java | 14 ++++- .../impl/DefaultWorkflowPositionFactory.java | 32 ++++++++++ .../serverlessworkflow/impl/TaskContext.java | 38 ++++++++++-- .../impl/WorkflowApplication.java | 15 ++++- .../impl/WorkflowContext.java | 43 +------------- .../impl/WorkflowDefinition.java | 13 ++++- .../impl/WorkflowFilter.java | 3 +- .../impl/WorkflowInstance.java | 18 +++--- .../impl/WorkflowPosition.java | 2 + .../impl/WorkflowPositionFactory.java | 20 +++++++ .../impl/WorkflowUtils.java | 32 +++++----- .../impl/executors/AbstractTaskExecutor.java | 15 ++--- .../executors/DefaultTaskExecutorFactory.java | 2 + .../impl/executors/DoExecutor.java | 2 +- .../impl/executors/ForExecutor.java | 58 +++++++++++++++++++ .../impl/executors/SetExecutor.java | 16 +++-- .../impl/executors/SwitchExecutor.java | 6 +- .../impl/executors/TaskExecutor.java | 8 ++- .../impl/expressions/Expression.java | 3 +- .../impl/expressions/ExpressionUtils.java | 8 +-- .../impl/expressions/JQExpression.java | 15 ++++- .../impl/WorkflowDefinitionTest.java | 19 ++++-- impl/core/src/test/resources/for-collect.yaml | 17 ++++++ impl/core/src/test/resources/for-sum.yaml | 19 ++++++ .../impl/executors/HttpExecutor.java | 11 ++-- 26 files changed, 332 insertions(+), 119 deletions(-) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPositionFactory.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java create mode 100644 impl/core/src/test/resources/for-collect.yaml create mode 100644 impl/core/src/test/resources/for-sum.yaml diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java b/impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java new file mode 100644 index 00000000..a58dc348 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java @@ -0,0 +1,22 @@ +/* + * 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; + +import java.util.Map; + +public interface ContextAware { + Map variables(); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java index 2e51f6a6..54f993b1 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java @@ -17,7 +17,19 @@ public class DefaultWorkflowPosition implements WorkflowPosition { - private StringBuilder sb = new StringBuilder(""); + private StringBuilder sb; + + DefaultWorkflowPosition() { + this.sb = new StringBuilder(""); + } + + private DefaultWorkflowPosition(WorkflowPosition position) { + this.sb = new StringBuilder(position.toString()); + } + + public DefaultWorkflowPosition copy() { + return new DefaultWorkflowPosition(this); + } @Override public WorkflowPosition addIndex(int index) { diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPositionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPositionFactory.java new file mode 100644 index 00000000..00b0085c --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPositionFactory.java @@ -0,0 +1,32 @@ +/* + * 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; + +class DefaultWorkflowPositionFactory implements WorkflowPositionFactory { + + private static WorkflowPositionFactory instance = new DefaultWorkflowPositionFactory(); + + public static WorkflowPositionFactory get() { + return instance; + } + + private DefaultWorkflowPositionFactory() {} + + @Override + public WorkflowPosition buildPosition() { + return new DefaultWorkflowPosition(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java index 91f6aa61..c23de49f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -19,28 +19,48 @@ import io.serverlessworkflow.api.types.FlowDirective; import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.TaskBase; +import java.util.HashMap; +import java.util.Map; -public class TaskContext { +public class TaskContext implements ContextAware { private final JsonNode rawInput; private final T task; + private final WorkflowPosition position; private JsonNode input; private JsonNode output; private JsonNode rawOutput; private FlowDirective flowDirective; + private Map contextVariables; - public TaskContext(JsonNode rawInput, T task) { - this.rawInput = rawInput; + public TaskContext(JsonNode input, WorkflowPosition position) { + this.rawInput = input; + this.position = position; + this.task = null; + this.contextVariables = new HashMap<>(); + init(); + } + + public TaskContext(JsonNode input, TaskContext taskContext, T task) { + this.rawInput = input; + this.position = taskContext.position.copy(); + this.task = task; + this.flowDirective = task.getThen(); + this.contextVariables = new HashMap<>(taskContext.variables()); + init(); + } + + private void init() { this.input = rawInput; this.rawOutput = rawInput; this.output = rawInput; - this.task = task; - this.flowDirective = task.getThen(); } public void input(JsonNode input) { this.input = input; + this.rawOutput = input; + this.output = input; } public JsonNode input() { @@ -81,4 +101,12 @@ public FlowDirective flowDirective() { ? new FlowDirective().withFlowDirectiveEnum(FlowDirectiveEnum.CONTINUE) : flowDirective; } + + public Map variables() { + return contextVariables; + } + + public WorkflowPosition position() { + return position; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java index 02d807f3..d9da16b9 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java @@ -25,7 +25,6 @@ import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; import io.serverlessworkflow.impl.resources.DefaultResourceLoaderFactory; import io.serverlessworkflow.impl.resources.ResourceLoaderFactory; - import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -40,17 +39,20 @@ public class WorkflowApplication implements AutoCloseable { private final SchemaValidatorFactory schemaValidatorFactory; private final Collection listeners; private final Map definitions; + private final WorkflowPositionFactory positionFactory; public WorkflowApplication( TaskExecutorFactory taskFactory, ExpressionFactory exprFactory, ResourceLoaderFactory resourceLoaderFactory, SchemaValidatorFactory schemaValidatorFactory, + WorkflowPositionFactory positionFactory, Collection listeners) { this.taskFactory = taskFactory; this.exprFactory = exprFactory; this.resourceLoaderFactory = resourceLoaderFactory; this.schemaValidatorFactory = schemaValidatorFactory; + this.positionFactory = positionFactory; this.listeners = listeners; this.definitions = new ConcurrentHashMap<>(); } @@ -85,6 +87,7 @@ public static class Builder { private Collection listeners; private ResourceLoaderFactory resourceLoaderFactory = DefaultResourceLoaderFactory.get(); private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get(); + private WorkflowPositionFactory positionFactory = DefaultWorkflowPositionFactory.get(); private Builder() {} @@ -111,6 +114,11 @@ public Builder withResourceLoaderFactory(ResourceLoaderFactory resourceLoader) { return this; } + public Builder withPositionFactory(WorkflowPositionFactory positionFactory) { + this.positionFactory = positionFactory; + return this; + } + public Builder withSchemaValidatorFactory(SchemaValidatorFactory factory) { this.schemaValidatorFactory = factory; return this; @@ -122,6 +130,7 @@ public WorkflowApplication build() { exprFactory, resourceLoaderFactory, schemaValidatorFactory, + positionFactory, listeners == null ? Collections.emptySet() : Collections.unmodifiableCollection(listeners)); @@ -146,4 +155,8 @@ public void close() throws Exception { } definitions.clear(); } + + public WorkflowPositionFactory positionFactory() { + return positionFactory; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java index 4f0f0f16..d5c1f428 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java @@ -19,49 +19,16 @@ import io.serverlessworkflow.impl.json.JsonUtils; public class WorkflowContext { - private final WorkflowPosition position; private final WorkflowDefinition definition; private final JsonNode input; - private JsonNode current; private JsonNode context; - private WorkflowContext( - WorkflowPosition position, WorkflowDefinition definition, JsonNode input) { - this.position = position; + WorkflowContext(WorkflowDefinition definition, JsonNode input) { this.definition = definition; this.input = input; - this.current = input.deepCopy(); this.context = JsonUtils.mapper().createObjectNode(); } - public static Builder builder(WorkflowDefinition definition, JsonNode input) { - return new Builder(definition, input); - } - - public static class Builder { - private WorkflowPosition position = new DefaultWorkflowPosition(); - private WorkflowDefinition definition; - private JsonNode input; - - private Builder(WorkflowDefinition definition, JsonNode input) { - this.definition = definition; - this.input = input; - } - - public Builder position(WorkflowPosition position) { - this.position = position; - return this; - } - - public WorkflowContext build() { - return new WorkflowContext(position, definition, input); - } - } - - public WorkflowPosition position() { - return position; - } - public JsonNode context() { return context; } @@ -74,14 +41,6 @@ public JsonNode rawInput() { return input; } - public void current(JsonNode output) { - this.current = output; - } - - public JsonNode current() { - return current; - } - public WorkflowDefinition definition() { return definition; } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index f593cea3..3a76ff1f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -28,7 +28,6 @@ import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; import io.serverlessworkflow.impl.resources.ResourceLoader; - import java.nio.file.Path; import java.util.Collection; import java.util.Map; @@ -47,6 +46,7 @@ public class WorkflowDefinition implements AutoCloseable { private final ExpressionFactory exprFactory; private final ResourceLoader resourceLoader; private final SchemaValidatorFactory schemaValidatorFactory; + private final WorkflowPositionFactory positionFactory; private final Map> taskExecutors = new ConcurrentHashMap<>(); @@ -56,12 +56,14 @@ private WorkflowDefinition( TaskExecutorFactory taskFactory, ResourceLoader resourceLoader, ExpressionFactory exprFactory, - SchemaValidatorFactory schemaValidatorFactory) { + SchemaValidatorFactory schemaValidatorFactory, + WorkflowPositionFactory positionFactory) { this.workflow = workflow; this.listeners = listeners; this.taskFactory = taskFactory; this.exprFactory = exprFactory; this.schemaValidatorFactory = schemaValidatorFactory; + this.positionFactory = positionFactory; this.resourceLoader = resourceLoader; if (workflow.getInput() != null) { Input input = workflow.getInput(); @@ -90,7 +92,8 @@ static WorkflowDefinition of(WorkflowApplication application, Workflow workflow, application.taskFactory(), application.resourceLoaderFactory().getResourceLoader(path), application.expressionFactory(), - application.validatorFactory()); + application.validatorFactory(), + application.positionFactory()); } public WorkflowInstance execute(Object input) { @@ -142,6 +145,10 @@ public ResourceLoader resourceLoader() { return resourceLoader; } + public WorkflowPositionFactory positionFactory() { + return positionFactory; + } + @Override public void close() { // TODO close resourcers hold for uncompleted process instances, if any diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java index 7fde97ba..7d25df48 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java @@ -16,9 +16,8 @@ package io.serverlessworkflow.impl; import com.fasterxml.jackson.databind.JsonNode; -import java.util.Optional; @FunctionalInterface public interface WorkflowFilter { - JsonNode apply(WorkflowContext workflow, Optional> task, JsonNode node); + JsonNode apply(WorkflowContext workflow, TaskContext task, JsonNode node); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index bd2f94b8..1361c43f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -18,24 +18,26 @@ import static io.serverlessworkflow.impl.json.JsonUtils.toJavaValue; import com.fasterxml.jackson.databind.JsonNode; -import java.util.Optional; public class WorkflowInstance { private WorkflowState state; private WorkflowContext context; + private TaskContext taskContext; WorkflowInstance(WorkflowDefinition definition, JsonNode input) { definition.inputSchemaValidator().ifPresent(v -> v.validate(input)); - context = WorkflowContext.builder(definition, input).build(); + context = new WorkflowContext(definition, input); + taskContext = new TaskContext<>(input, definition.positionFactory().buildPosition()); definition .inputFilter() - .ifPresent(f -> context.current(f.apply(context, Optional.empty(), context.current()))); + .ifPresent(f -> taskContext.input(f.apply(context, taskContext, input))); state = WorkflowState.STARTED; - WorkflowUtils.processTaskList(definition.workflow().getDo(), context); + taskContext.rawOutput( + WorkflowUtils.processTaskList(definition.workflow().getDo(), context, taskContext)); definition .outputFilter() - .ifPresent(f -> context.current(f.apply(context, Optional.empty(), context.current()))); - definition.outputSchemaValidator().ifPresent(v -> v.validate(context.current())); + .ifPresent(f -> taskContext.output(f.apply(context, taskContext, taskContext.rawOutput()))); + definition.outputSchemaValidator().ifPresent(v -> v.validate(taskContext.output())); } public WorkflowState state() { @@ -43,10 +45,10 @@ public WorkflowState state() { } public Object output() { - return toJavaValue(context.current()); + return toJavaValue(taskContext.output()); } public Object outputAsJsonNode() { - return context.current(); + return taskContext.output(); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java index c43d4b2f..cf63844a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java @@ -24,4 +24,6 @@ public interface WorkflowPosition { WorkflowPosition addIndex(int index); WorkflowPosition back(); + + WorkflowPosition copy(); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java new file mode 100644 index 00000000..e93a4c33 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface WorkflowPositionFactory { + WorkflowPosition buildPosition(); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java index 259a0e6a..54abd7e7 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -35,7 +35,6 @@ import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; import io.serverlessworkflow.impl.resources.ResourceLoader; import io.serverlessworkflow.impl.resources.StaticResource; - import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -128,32 +127,33 @@ private static TaskItem findTaskByName(ListIterator iter, String taskN throw new IllegalArgumentException("Cannot find task with name " + taskName); } - public static void processTaskList(List tasks, WorkflowContext context) { - context.position().addProperty("do"); + public static JsonNode processTaskList( + List tasks, WorkflowContext context, TaskContext parentTask) { + parentTask.position().addProperty("do"); + TaskContext currentContext = parentTask; if (!tasks.isEmpty()) { ListIterator iter = tasks.listIterator(); TaskItem nextTask = iter.next(); while (nextTask != null) { TaskItem task = nextTask; - context.position().addIndex(iter.nextIndex()).addProperty(task.getName()); + parentTask.position().addIndex(iter.nextIndex()).addProperty(task.getName()); context .definition() .listeners() - .forEach(l -> l.onTaskStarted(context.position(), task.getTask())); - TaskContext taskContext = + .forEach(l -> l.onTaskStarted(parentTask.position(), task.getTask())); + currentContext = context .definition() .taskExecutors() .computeIfAbsent( - context.position().jsonPointer(), + parentTask.position().jsonPointer(), k -> context .definition() .taskFactory() .getTaskExecutor(task.getTask(), context.definition())) - .apply(context, context.current()); - context.current(taskContext.output()); - FlowDirective flowDirective = taskContext.flowDirective(); + .apply(context, parentTask, currentContext.output()); + FlowDirective flowDirective = currentContext.flowDirective(); if (flowDirective.getFlowDirectiveEnum() != null) { switch (flowDirective.getFlowDirectiveEnum()) { case CONTINUE: @@ -170,15 +170,21 @@ public static void processTaskList(List tasks, WorkflowContext context context .definition() .listeners() - .forEach(l -> l.onTaskEnded(context.position(), task.getTask())); - context.position().back(); + .forEach(l -> l.onTaskEnded(parentTask.position(), task.getTask())); + parentTask.position().back(); } } - context.position().back(); + parentTask.position().back(); + return currentContext.output(); } public static WorkflowFilter buildWorkflowFilter(ExpressionFactory exprFactory, String str) { + assert str != null; Expression expression = exprFactory.getExpression(str); return expression::eval; } + + public static Optional optionalFilter(ExpressionFactory exprFactory, String str) { + return str != null ? Optional.of(buildWorkflowFilter(exprFactory, str)) : Optional.empty(); + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java index 56e55120..086c7c51 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -83,23 +83,20 @@ private void buildContextProcessors(WorkflowDefinition definition) { } @Override - public TaskContext apply(WorkflowContext workflowContext, JsonNode rawInput) { - TaskContext taskContext = new TaskContext<>(rawInput, task); + public TaskContext apply( + WorkflowContext workflowContext, TaskContext parentContext, JsonNode input) { + TaskContext taskContext = new TaskContext<>(input, parentContext, task); inputSchemaValidator.ifPresent(s -> s.validate(taskContext.rawInput())); inputProcessor.ifPresent( - p -> - taskContext.input( - p.apply(workflowContext, Optional.of(taskContext), taskContext.rawInput()))); + p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); internalExecute(workflowContext, taskContext); outputProcessor.ifPresent( - p -> - taskContext.output( - p.apply(workflowContext, Optional.of(taskContext), taskContext.rawOutput()))); + p -> taskContext.output(p.apply(workflowContext, taskContext, taskContext.rawOutput()))); outputSchemaValidator.ifPresent(s -> s.validate(taskContext.output())); contextProcessor.ifPresent( p -> workflowContext.context( - p.apply(workflowContext, Optional.of(taskContext), workflowContext.context()))); + p.apply(workflowContext, taskContext, workflowContext.context()))); contextSchemaValidator.ifPresent(s -> s.validate(workflowContext.context())); return taskContext; } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index b5ac0119..45a988a7 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -65,6 +65,8 @@ public TaskExecutor getTaskExecutor( return new DoExecutor(task.getDoTask(), definition); } else if (task.getSetTask() != null) { return new SetExecutor(task.getSetTask(), definition); + } else if (task.getForTask() != null) { + return new ForExecutor(task.getForTask(), definition); } throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java index 30f35f95..df364c14 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java @@ -29,6 +29,6 @@ protected DoExecutor(DoTask task, WorkflowDefinition definition) { @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { - WorkflowUtils.processTaskList(task.getDo(), workflow); + taskContext.rawOutput(WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext)); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java new file mode 100644 index 00000000..511575f3 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java @@ -0,0 +1,58 @@ +/* + * 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.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.ForTask; +import io.serverlessworkflow.api.types.ForTaskConfiguration; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowUtils; +import java.util.Iterator; +import java.util.Optional; + +public class ForExecutor extends AbstractTaskExecutor { + + private final WorkflowFilter collectionExpr; + private final Optional whileExpr; + + protected ForExecutor(ForTask task, WorkflowDefinition definition) { + super(task, definition); + ForTaskConfiguration forConfig = task.getFor(); + this.collectionExpr = + WorkflowUtils.buildWorkflowFilter(definition.expressionFactory(), forConfig.getIn()); + this.whileExpr = WorkflowUtils.optionalFilter(definition.expressionFactory(), task.getWhile()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + Iterator iter = + collectionExpr.apply(workflow, taskContext, taskContext.input()).iterator(); + int i = 0; + while (iter.hasNext() + && whileExpr + .map(w -> w.apply(workflow, taskContext, taskContext.rawOutput())) + .map(n -> n.asBoolean(true)) + .orElse(true)) { + JsonNode item = iter.next(); + taskContext.variables().put(task.getFor().getEach(), item); + taskContext.variables().put(task.getFor().getAt(), i++); + taskContext.rawOutput(WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext)); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java index b9d9db86..0f0d999e 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java @@ -15,25 +15,33 @@ */ package io.serverlessworkflow.impl.executors; -import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.SetTask; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; import io.serverlessworkflow.impl.json.JsonUtils; import io.serverlessworkflow.impl.json.MergeUtils; +import java.util.Map; public class SetExecutor extends AbstractTaskExecutor { - private JsonNode toBeSet; + private Map toBeSet; protected SetExecutor(SetTask task, WorkflowDefinition definition) { super(task, definition); - this.toBeSet = JsonUtils.fromValue(task.getSet().getAdditionalProperties()); + this.toBeSet = + ExpressionUtils.buildExpressionMap( + task.getSet().getAdditionalProperties(), definition.expressionFactory()); } @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { - taskContext.rawOutput(MergeUtils.merge(toBeSet, taskContext.input())); + taskContext.rawOutput( + MergeUtils.merge( + JsonUtils.fromValue( + ExpressionUtils.evaluateExpressionMap( + toBeSet, workflow, taskContext, taskContext.input())), + taskContext.input())); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java index f081dfa4..dee0cee7 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java @@ -26,7 +26,6 @@ import io.serverlessworkflow.impl.WorkflowUtils; import java.util.Map; import java.util.Map.Entry; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; public class SwitchExecutor extends AbstractTaskExecutor { @@ -52,10 +51,7 @@ protected SwitchExecutor(SwitchTask task, WorkflowDefinition definition) { @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { for (Entry entry : workflowFilters.entrySet()) { - if (entry - .getValue() - .apply(workflow, Optional.of(taskContext), taskContext.input()) - .asBoolean()) { + if (entry.getValue().apply(workflow, taskContext, taskContext.input()).asBoolean()) { taskContext.flowDirective(entry.getKey().getThen()); return; } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java index 62228199..b4b66a9a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java @@ -19,7 +19,9 @@ import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; -import java.util.function.BiFunction; -public interface TaskExecutor - extends BiFunction> {} +@FunctionalInterface +public interface TaskExecutor { + TaskContext apply( + WorkflowContext workflowContext, TaskContext parentContext, JsonNode input); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java index f9d799ec..42566c77 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java @@ -18,8 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; -import java.util.Optional; public interface Expression { - JsonNode eval(WorkflowContext workflowContext, Optional> context, JsonNode node); + JsonNode eval(WorkflowContext workflowContext, TaskContext context, JsonNode node); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java index 72b4e90c..7f776322 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java @@ -20,7 +20,6 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.json.JsonUtils; import java.util.Map; -import java.util.Optional; public class ExpressionUtils { @@ -35,10 +34,7 @@ public static Map buildExpressionMap( } public static Map evaluateExpressionMap( - Map origMap, - WorkflowContext workflow, - Optional> task, - JsonNode n) { + Map origMap, WorkflowContext workflow, TaskContext task, JsonNode n) { return new ProxyMap( origMap, o -> @@ -54,7 +50,7 @@ public static Object buildExpressionObject(Object obj, ExpressionFactory factory } public static Object evaluateExpressionObject( - Object obj, WorkflowContext workflow, Optional> task, JsonNode node) { + Object obj, WorkflowContext workflow, TaskContext task, JsonNode node) { return obj instanceof Map ? ExpressionUtils.evaluateExpressionMap((Map) obj, workflow, task, node) : obj; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java index 7b008fcb..ae6c784f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import io.serverlessworkflow.impl.ContextAware; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.json.JsonUtils; @@ -177,10 +178,10 @@ public JsonNode getResult() { } @Override - public JsonNode eval(WorkflowContext workflow, Optional> task, JsonNode node) { + public JsonNode eval(WorkflowContext workflow, TaskContext task, JsonNode node) { TypedOutput output = output(JsonNode.class); try { - internalExpr.apply(this.scope.get(), node, output); + internalExpr.apply(createScope(workflow, task), node, output); return output.getResult(); } catch (JsonQueryException e) { throw new IllegalArgumentException( @@ -188,6 +189,16 @@ public JsonNode eval(WorkflowContext workflow, Optional> task, Js } } + private Scope createScope(WorkflowContext workflow, TaskContext task) { + return createScope(scope.get(), task); + } + + private Scope createScope(Scope parentScope, ContextAware context) { + Scope childScope = Scope.newChildScope(parentScope); + context.variables().forEach((k, v) -> childScope.setValue(k, JsonUtils.fromValue(v))); + return childScope; + } + private void checkFunctionCall(net.thisptr.jackson.jq.Expression toCheck) throws JsonQueryException { if (toCheck instanceof FunctionCall) { diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index f6c3455a..39adcf9d 100644 --- a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; +import java.util.Arrays; import java.util.Map; import java.util.stream.Stream; import org.assertj.core.api.Condition; @@ -45,10 +46,6 @@ void testWorkflowExecution(String fileName, Object input, Condition cond } private static Stream provideParameters() { - Map petInput = Map.of("petId", 10); - Condition petCondition = - new Condition<>( - o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"); return Stream.of( Arguments.of( "switch-then-string.yaml", @@ -82,6 +79,18 @@ private static Stream provideParameters() { o.equals( Map.of( "orderType", "unknown", "log", "warn", "message", "something's wrong")), - "switch-unknown"))); + "switch-unknown")), + Arguments.of( + "for-sum.yaml", + Map.of("input", Arrays.asList(1, 2, 3)), + new Condition(o -> o.equals(6), "for-sum")), + Arguments.of( + "for-collect.yaml", + Map.of("input", Arrays.asList(1, 2, 3)), + new Condition( + o -> + o.equals( + Map.of("input", Arrays.asList(1, 2, 3), "output", Arrays.asList(2, 4, 6))), + "for-collect"))); } } diff --git a/impl/core/src/test/resources/for-collect.yaml b/impl/core/src/test/resources/for-collect.yaml new file mode 100644 index 00000000..7bcc48c2 --- /dev/null +++ b/impl/core/src/test/resources/for-collect.yaml @@ -0,0 +1,17 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: for-collect-example + version: '0.1.0' +do: + - sumAll: + for: + each: number + in: .input + at: index + input: + from: '{input: .input, output: []}' + do: + - sumIndex: + output: + as: .output+=[$number+$index+1] \ No newline at end of file diff --git a/impl/core/src/test/resources/for-sum.yaml b/impl/core/src/test/resources/for-sum.yaml new file mode 100644 index 00000000..e0fe106b --- /dev/null +++ b/impl/core/src/test/resources/for-sum.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: for-sum-example + version: '0.1.0' +do: + - initCounter: + set: + counter: 0 + - sumAll: + for: + each: number + in: .input + do: + - accumulate: + output: + as: .counter+=$number + output: + as: .counter \ No newline at end of file diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index 684acd47..5ecd27af 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -38,7 +38,6 @@ import jakarta.ws.rs.client.WebTarget; import java.util.Map; import java.util.Map.Entry; -import java.util.Optional; public class HttpExecutor implements CallableTask { @@ -82,8 +81,7 @@ public void init(CallHTTP task, WorkflowDefinition definition) { (request, workflow, context, node) -> request.post( Entity.json( - ExpressionUtils.evaluateExpressionObject( - body, workflow, Optional.of(context), node)), + ExpressionUtils.evaluateExpressionObject(body, workflow, context, node)), JsonNode.class); break; case HttpMethod.GET: @@ -97,12 +95,11 @@ public JsonNode apply( WorkflowContext workflow, TaskContext taskContext, JsonNode input) { WebTarget target = targetSupplier.apply(workflow, taskContext, input); for (Entry entry : - ExpressionUtils.evaluateExpressionMap(queryMap, workflow, Optional.of(taskContext), input) - .entrySet()) { + ExpressionUtils.evaluateExpressionMap(queryMap, workflow, taskContext, input).entrySet()) { target = target.queryParam(entry.getKey(), entry.getValue()); } Builder request = target.request(); - ExpressionUtils.evaluateExpressionMap(headersMap, workflow, Optional.of(taskContext), input) + ExpressionUtils.evaluateExpressionMap(headersMap, workflow, taskContext, input) .forEach(request::header); return requestFunction.apply(request, workflow, taskContext, input); } @@ -153,7 +150,7 @@ public ExpressionURISupplier(Expression expr) { @Override public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { - return client.target(expr.eval(workflow, Optional.of(task), node).asText()); + return client.target(expr.eval(workflow, task, node).asText()); } } } From 251dd0610278244d9947b413a8515cd10987039f Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Tue, 26 Nov 2024 19:55:22 +0100 Subject: [PATCH 15/22] Jq class clean up Signed-off-by: Francisco Javier Tirado Sarti --- .../serverlessworkflow/impl/ContextAware.java | 22 --- .../serverlessworkflow/impl/TaskContext.java | 2 +- .../impl/expressions/JQExpression.java | 177 +----------------- impl/pom.xml | 6 +- 4 files changed, 8 insertions(+), 199 deletions(-) delete mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java b/impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java deleted file mode 100644 index a58dc348..00000000 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/ContextAware.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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; - -import java.util.Map; - -public interface ContextAware { - Map variables(); -} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java index c23de49f..138a4aed 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -22,7 +22,7 @@ import java.util.HashMap; import java.util.Map; -public class TaskContext implements ContextAware { +public class TaskContext { private final JsonNode rawInput; private final T task; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java index ae6c784f..9da21dbe 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java @@ -17,141 +17,38 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import io.serverlessworkflow.impl.ContextAware; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.json.JsonUtils; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import net.thisptr.jackson.jq.Output; import net.thisptr.jackson.jq.Scope; import net.thisptr.jackson.jq.Version; import net.thisptr.jackson.jq.exception.JsonQueryException; import net.thisptr.jackson.jq.internal.javacc.ExpressionParser; -import net.thisptr.jackson.jq.internal.tree.FunctionCall; -import net.thisptr.jackson.jq.internal.tree.StringInterpolation; -import net.thisptr.jackson.jq.internal.tree.binaryop.BinaryOperatorExpression; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class JQExpression implements Expression { - private static final Logger logger = LoggerFactory.getLogger(JQExpression.class); - private final Map, Collection> - declaredFieldsMap = new ConcurrentHashMap<>(); - private final Map, Collection> - allFieldsMap = new ConcurrentHashMap<>(); - private final Supplier scope; private final String expr; private net.thisptr.jackson.jq.Expression internalExpr; - private static Field rhsField; - - static { - try { - rhsField = BinaryOperatorExpression.class.getDeclaredField("rhs"); - rhsField.setAccessible(true); - } catch (ReflectiveOperationException e) { - logger.warn("Unexpected exception while resolving rhs field", e); - } - } public JQExpression(Supplier scope, String expr, Version version) throws JsonQueryException { this.expr = expr; this.scope = scope; this.internalExpr = compile(version); - checkFunctionCall(internalExpr); } private net.thisptr.jackson.jq.Expression compile(Version version) throws JsonQueryException { - net.thisptr.jackson.jq.Expression expression; - try { - expression = ExpressionParser.compile(expr, version); - } catch (JsonQueryException ex) { - expression = handleStringInterpolation(version).orElseThrow(() -> ex); - } - checkFunctionCall(expression); - return expression; - } - - private Optional handleStringInterpolation(Version version) { - if (!expr.startsWith("\"")) { - try { - net.thisptr.jackson.jq.Expression expression = - ExpressionParser.compile("\"" + expr + "\"", version); - if (expression instanceof StringInterpolation) { - return Optional.of(expression); - } - } catch (JsonQueryException ex) { - // ignoring it - } - } - return Optional.empty(); + return ExpressionParser.compile(expr, version); } private interface TypedOutput extends Output { T getResult(); } - @SuppressWarnings("unchecked") - private TypedOutput output(Class returnClass) { - TypedOutput out; - if (String.class.isAssignableFrom(returnClass)) { - out = (TypedOutput) new StringOutput(); - } else if (Collection.class.isAssignableFrom(returnClass)) { - out = (TypedOutput) new CollectionOutput(); - } else { - out = (TypedOutput) new JsonNodeOutput(); - } - return out; - } - - private static class StringOutput implements TypedOutput { - StringBuilder sb = new StringBuilder(); - - @Override - public void emit(JsonNode out) throws JsonQueryException { - if (sb.length() > 0) { - sb.append(' '); - } - if (!out.isNull() && out.asText() != null) { - sb.append(out.asText()); - } - } - - @Override - public String getResult() { - return sb.toString(); - } - } - - private static class CollectionOutput implements TypedOutput> { - Collection result = new ArrayList<>(); - - @SuppressWarnings("unchecked") - @Override - public void emit(JsonNode out) throws JsonQueryException { - Object obj = JsonUtils.toJavaValue(out); - if (obj instanceof Collection) result.addAll((Collection) obj); - else { - result.add(obj); - } - } - - @Override - public Collection getResult() { - return result; - } - } - private static class JsonNodeOutput implements TypedOutput { private JsonNode result; @@ -179,7 +76,7 @@ public JsonNode getResult() { @Override public JsonNode eval(WorkflowContext workflow, TaskContext task, JsonNode node) { - TypedOutput output = output(JsonNode.class); + TypedOutput output = new JsonNodeOutput(); try { internalExpr.apply(createScope(workflow, task), node, output); return output.getResult(); @@ -190,74 +87,8 @@ public JsonNode eval(WorkflowContext workflow, TaskContext task, JsonNode nod } private Scope createScope(WorkflowContext workflow, TaskContext task) { - return createScope(scope.get(), task); - } - - private Scope createScope(Scope parentScope, ContextAware context) { - Scope childScope = Scope.newChildScope(parentScope); - context.variables().forEach((k, v) -> childScope.setValue(k, JsonUtils.fromValue(v))); + Scope childScope = Scope.newChildScope(scope.get()); + task.variables().forEach((k, v) -> childScope.setValue(k, JsonUtils.fromValue(v))); return childScope; } - - private void checkFunctionCall(net.thisptr.jackson.jq.Expression toCheck) - throws JsonQueryException { - if (toCheck instanceof FunctionCall) { - toCheck.apply(scope.get(), JsonUtils.mapper().createObjectNode(), out -> {}); - } else if (toCheck instanceof BinaryOperatorExpression) { - if (rhsField != null) { - try { - checkFunctionCall((net.thisptr.jackson.jq.Expression) rhsField.get(toCheck)); - } catch (ReflectiveOperationException e) { - logger.warn( - "Ignoring unexpected error {} while accesing field {} for class{} and expression {}", - e.getMessage(), - rhsField.getName(), - toCheck.getClass(), - expr); - } - } - } else if (toCheck != null) { - for (Field f : getAllExprFields(toCheck)) - try { - checkFunctionCall((net.thisptr.jackson.jq.Expression) f.get(toCheck)); - } catch (ReflectiveOperationException e) { - logger.warn( - "Ignoring unexpected error {} while accesing field {} for class{} and expression {}", - e.getMessage(), - f.getName(), - toCheck.getClass(), - expr); - } - } - } - - private Collection getAllExprFields(net.thisptr.jackson.jq.Expression toCheck) { - return allFieldsMap.computeIfAbsent(toCheck.getClass(), this::getAllExprFields); - } - - private Collection getAllExprFields( - Class clazz) { - Collection fields = new HashSet<>(); - Class currentClass = clazz; - do { - fields.addAll( - declaredFieldsMap.computeIfAbsent( - currentClass.asSubclass(net.thisptr.jackson.jq.Expression.class), - this::getDeclaredExprFields)); - currentClass = currentClass.getSuperclass(); - } while (net.thisptr.jackson.jq.Expression.class.isAssignableFrom(currentClass)); - return fields; - } - - private Collection getDeclaredExprFields( - Class clazz) { - Collection fields = new HashSet<>(); - for (Field f : clazz.getDeclaredFields()) { - if (net.thisptr.jackson.jq.Expression.class.isAssignableFrom(f.getType())) { - f.setAccessible(true); - fields.add(f); - } - } - return fields; - } } diff --git a/impl/pom.xml b/impl/pom.xml index 191dc39d..b49f8ab0 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -35,8 +35,8 @@ - http - core - bom + http + core + bom \ No newline at end of file From 997f4236fee1b2c4e506d77d639212fb2ef5f458 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Wed, 27 Nov 2024 17:07:39 +0100 Subject: [PATCH 16/22] [Fix #463] Supporting runtime expressions Signed-off-by: Francisco Javier Tirado Sarti --- impl/core/pom.xml | 6 ++ .../impl/QueueWorkflowPosition.java | 70 ++++++++++++++++ ...ory.java => RuntimeDescriptorFactory.java} | 18 +--- ...java => StringBufferWorkflowPosition.java} | 22 +++-- .../serverlessworkflow/impl/TaskContext.java | 6 ++ .../impl/WorkflowApplication.java | 33 +++++++- .../impl/WorkflowContext.java | 21 ++--- .../impl/WorkflowDefinition.java | 57 +++++-------- .../impl/WorkflowIdFactory.java | 21 +++++ .../impl/WorkflowInstance.java | 42 ++++++++-- .../impl/WorkflowPosition.java | 2 + .../impl/WorkflowPositionFactory.java | 6 +- .../impl/expressions/DateTimeDescriptor.java | 48 +++++++++++ .../impl/expressions/JQExpression.java | 45 +++++----- .../impl/expressions/RuntimeDescriptor.java | 21 +++++ .../impl/expressions/TaskDescriptor.java | 39 +++++++++ .../impl/expressions/WorkflowDescriptor.java | 33 ++++++++ .../impl/WorkflowDefinitionTest.java | 83 ++++++++++++------- .../src/test/resources/simple-expression.yaml | 11 +++ 19 files changed, 452 insertions(+), 132 deletions(-) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java rename impl/core/src/main/java/io/serverlessworkflow/impl/{DefaultWorkflowPositionFactory.java => RuntimeDescriptorFactory.java} (63%) rename impl/core/src/main/java/io/serverlessworkflow/impl/{DefaultWorkflowPosition.java => StringBufferWorkflowPosition.java} (69%) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowIdFactory.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/expressions/DateTimeDescriptor.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/expressions/RuntimeDescriptor.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/expressions/TaskDescriptor.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/expressions/WorkflowDescriptor.java create mode 100644 impl/core/src/test/resources/simple-expression.yaml diff --git a/impl/core/pom.xml b/impl/core/pom.xml index 597b3758..940f79fa 100644 --- a/impl/core/pom.xml +++ b/impl/core/pom.xml @@ -8,6 +8,7 @@ serverlessworkflow-impl-core 1.1.0 + 5.2.3 @@ -15,6 +16,11 @@ serverlessworkflow-api 7.0.0-SNAPSHOT + + com.github.f4b6a3 + ulid-creator + ${version.com.github.f4b6a3} + com.networknt json-schema-validator diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java new file mode 100644 index 00000000..c6d3f141 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java @@ -0,0 +1,70 @@ +/* + * 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; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.stream.Collectors; + +public class QueueWorkflowPosition implements WorkflowPosition { + + private Deque queue; + + QueueWorkflowPosition() { + this(new ArrayDeque<>()); + } + + private QueueWorkflowPosition(Deque list) { + this.queue = list; + } + + public QueueWorkflowPosition copy() { + return new QueueWorkflowPosition(new ArrayDeque<>(this.queue)); + } + + @Override + public WorkflowPosition addIndex(int index) { + queue.add(index); + return this; + } + + @Override + public WorkflowPosition addProperty(String prop) { + queue.add(prop); + return this; + } + + @Override + public String jsonPointer() { + return queue.stream().map(Object::toString).collect(Collectors.joining("/")); + } + + @Override + public String toString() { + return "ListWorkflowPosition [list=" + queue + "]"; + } + + @Override + public WorkflowPosition back() { + queue.removeLast(); + return this; + } + + @Override + public Object last() { + return queue.pollLast(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPositionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/RuntimeDescriptorFactory.java similarity index 63% rename from impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPositionFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/RuntimeDescriptorFactory.java index 00b0085c..2d0601fb 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPositionFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/RuntimeDescriptorFactory.java @@ -15,18 +15,8 @@ */ package io.serverlessworkflow.impl; -class DefaultWorkflowPositionFactory implements WorkflowPositionFactory { +import io.serverlessworkflow.impl.expressions.RuntimeDescriptor; +import java.util.function.Supplier; - private static WorkflowPositionFactory instance = new DefaultWorkflowPositionFactory(); - - public static WorkflowPositionFactory get() { - return instance; - } - - private DefaultWorkflowPositionFactory() {} - - @Override - public WorkflowPosition buildPosition() { - return new DefaultWorkflowPosition(); - } -} +@FunctionalInterface +public interface RuntimeDescriptorFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/StringBufferWorkflowPosition.java similarity index 69% rename from impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/StringBufferWorkflowPosition.java index 54f993b1..18aaf8e4 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/StringBufferWorkflowPosition.java @@ -15,20 +15,20 @@ */ package io.serverlessworkflow.impl; -public class DefaultWorkflowPosition implements WorkflowPosition { +public class StringBufferWorkflowPosition implements WorkflowPosition { private StringBuilder sb; - DefaultWorkflowPosition() { - this.sb = new StringBuilder(""); + StringBufferWorkflowPosition() { + this(""); } - private DefaultWorkflowPosition(WorkflowPosition position) { - this.sb = new StringBuilder(position.toString()); + private StringBufferWorkflowPosition(String str) { + this.sb = new StringBuilder(str); } - public DefaultWorkflowPosition copy() { - return new DefaultWorkflowPosition(this); + public StringBufferWorkflowPosition copy() { + return new StringBufferWorkflowPosition(this.jsonPointer()); } @Override @@ -50,7 +50,7 @@ public String jsonPointer() { @Override public String toString() { - return "DefaultWorkflowPosition [sb=" + sb + "]"; + return "StringBufferWorkflowPosition [sb=" + sb + "]"; } @Override @@ -61,4 +61,10 @@ public WorkflowPosition back() { } return this; } + + @Override + public Object last() { + int indexOf = sb.lastIndexOf("/"); + return indexOf != -1 ? jsonPointer().substring(indexOf + 1) : ""; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java index 138a4aed..cadde89c 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -19,6 +19,7 @@ import io.serverlessworkflow.api.types.FlowDirective; import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.TaskBase; +import java.time.Instant; import java.util.HashMap; import java.util.Map; @@ -27,6 +28,7 @@ public class TaskContext { private final JsonNode rawInput; private final T task; private final WorkflowPosition position; + private final Instant startedAt = Instant.now(); private JsonNode input; private JsonNode output; @@ -109,4 +111,8 @@ public Map variables() { public WorkflowPosition position() { return position; } + + public Instant startedAt() { + return startedAt; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java index d9da16b9..4f35de41 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java @@ -15,12 +15,14 @@ */ package io.serverlessworkflow.impl; +import com.github.f4b6a3.ulid.UlidCreator; import io.serverlessworkflow.api.types.Document; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; import io.serverlessworkflow.impl.executors.TaskExecutorFactory; import io.serverlessworkflow.impl.expressions.ExpressionFactory; import io.serverlessworkflow.impl.expressions.JQExpressionFactory; +import io.serverlessworkflow.impl.expressions.RuntimeDescriptor; import io.serverlessworkflow.impl.jsonschema.DefaultSchemaValidatorFactory; import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; import io.serverlessworkflow.impl.resources.DefaultResourceLoaderFactory; @@ -37,9 +39,11 @@ public class WorkflowApplication implements AutoCloseable { private final ExpressionFactory exprFactory; private final ResourceLoaderFactory resourceLoaderFactory; private final SchemaValidatorFactory schemaValidatorFactory; + private final WorkflowIdFactory idFactory; private final Collection listeners; private final Map definitions; private final WorkflowPositionFactory positionFactory; + private final RuntimeDescriptorFactory runtimeDescriptorFactory; public WorkflowApplication( TaskExecutorFactory taskFactory, @@ -47,12 +51,16 @@ public WorkflowApplication( ResourceLoaderFactory resourceLoaderFactory, SchemaValidatorFactory schemaValidatorFactory, WorkflowPositionFactory positionFactory, + WorkflowIdFactory idFactory, + RuntimeDescriptorFactory runtimeDescriptorFactory, Collection listeners) { this.taskFactory = taskFactory; this.exprFactory = exprFactory; this.resourceLoaderFactory = resourceLoaderFactory; this.schemaValidatorFactory = schemaValidatorFactory; this.positionFactory = positionFactory; + this.idFactory = idFactory; + this.runtimeDescriptorFactory = runtimeDescriptorFactory; this.listeners = listeners; this.definitions = new ConcurrentHashMap<>(); } @@ -81,13 +89,20 @@ public Collection listeners() { return listeners; } + public WorkflowIdFactory idFactory() { + return idFactory; + } + public static class Builder { private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get(); private ExpressionFactory exprFactory = JQExpressionFactory.get(); private Collection listeners; private ResourceLoaderFactory resourceLoaderFactory = DefaultResourceLoaderFactory.get(); private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get(); - private WorkflowPositionFactory positionFactory = DefaultWorkflowPositionFactory.get(); + private WorkflowPositionFactory positionFactory = () -> new QueueWorkflowPosition(); + private WorkflowIdFactory idFactory = () -> UlidCreator.getMonotonicUlid().toString(); + private RuntimeDescriptorFactory descriptorFactory = + () -> new RuntimeDescriptor("reference impl", "1.0.0_alpha", Collections.emptyMap()); private Builder() {} @@ -124,6 +139,16 @@ public Builder withSchemaValidatorFactory(SchemaValidatorFactory factory) { return this; } + public Builder withIdFactory(WorkflowIdFactory factory) { + this.idFactory = factory; + return this; + } + + public Builder withDescriptorFactory(RuntimeDescriptorFactory factory) { + this.descriptorFactory = factory; + return this; + } + public WorkflowApplication build() { return new WorkflowApplication( taskFactory, @@ -131,6 +156,8 @@ public WorkflowApplication build() { resourceLoaderFactory, schemaValidatorFactory, positionFactory, + idFactory, + descriptorFactory, listeners == null ? Collections.emptySet() : Collections.unmodifiableCollection(listeners)); @@ -159,4 +186,8 @@ public void close() throws Exception { public WorkflowPositionFactory positionFactory() { return positionFactory; } + + public RuntimeDescriptorFactory runtimeDescriptorFactory() { + return runtimeDescriptorFactory; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java index d5c1f428..f45f1b84 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java @@ -16,29 +16,26 @@ package io.serverlessworkflow.impl; import com.fasterxml.jackson.databind.JsonNode; -import io.serverlessworkflow.impl.json.JsonUtils; public class WorkflowContext { private final WorkflowDefinition definition; - private final JsonNode input; - private JsonNode context; + private final WorkflowInstance instance; - WorkflowContext(WorkflowDefinition definition, JsonNode input) { + WorkflowContext(WorkflowDefinition definition, WorkflowInstance instance) { this.definition = definition; - this.input = input; - this.context = JsonUtils.mapper().createObjectNode(); + this.instance = instance; } - public JsonNode context() { - return context; + public WorkflowInstance instance() { + return instance; } - public void context(JsonNode context) { - this.context = context; + public JsonNode context() { + return instance.context(); } - public JsonNode rawInput() { - return input; + public void context(JsonNode context) { + this.instance.context(context); } public WorkflowDefinition definition() { diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index 3a76ff1f..db8b5e4d 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -37,47 +37,34 @@ public class WorkflowDefinition implements AutoCloseable { private final Workflow workflow; - private final Collection listeners; private Optional inputSchemaValidator = Optional.empty(); private Optional outputSchemaValidator = Optional.empty(); private Optional inputFilter = Optional.empty(); private Optional outputFilter = Optional.empty(); - private final TaskExecutorFactory taskFactory; - private final ExpressionFactory exprFactory; - private final ResourceLoader resourceLoader; - private final SchemaValidatorFactory schemaValidatorFactory; - private final WorkflowPositionFactory positionFactory; private final Map> taskExecutors = new ConcurrentHashMap<>(); + private final ResourceLoader resourceLoader; + private final WorkflowApplication application; private WorkflowDefinition( - Workflow workflow, - Collection listeners, - TaskExecutorFactory taskFactory, - ResourceLoader resourceLoader, - ExpressionFactory exprFactory, - SchemaValidatorFactory schemaValidatorFactory, - WorkflowPositionFactory positionFactory) { + WorkflowApplication application, Workflow workflow, ResourceLoader resourceLoader) { + this.workflow = workflow; - this.listeners = listeners; - this.taskFactory = taskFactory; - this.exprFactory = exprFactory; - this.schemaValidatorFactory = schemaValidatorFactory; - this.positionFactory = positionFactory; + this.application = application; this.resourceLoader = resourceLoader; if (workflow.getInput() != null) { Input input = workflow.getInput(); this.inputSchemaValidator = getSchemaValidator( - schemaValidatorFactory, schemaToNode(resourceLoader, input.getSchema())); - this.inputFilter = buildWorkflowFilter(exprFactory, input.getFrom()); + application.validatorFactory(), schemaToNode(resourceLoader, input.getSchema())); + this.inputFilter = buildWorkflowFilter(application.expressionFactory(), input.getFrom()); } if (workflow.getOutput() != null) { Output output = workflow.getOutput(); this.outputSchemaValidator = getSchemaValidator( - schemaValidatorFactory, schemaToNode(resourceLoader, output.getSchema())); - this.outputFilter = buildWorkflowFilter(exprFactory, output.getAs()); + application.validatorFactory(), schemaToNode(resourceLoader, output.getSchema())); + this.outputFilter = buildWorkflowFilter(application.expressionFactory(), output.getAs()); } } @@ -87,13 +74,7 @@ static WorkflowDefinition of(WorkflowApplication application, Workflow workflow) static WorkflowDefinition of(WorkflowApplication application, Workflow workflow, Path path) { return new WorkflowDefinition( - workflow, - application.listeners(), - application.taskFactory(), - application.resourceLoaderFactory().getResourceLoader(path), - application.expressionFactory(), - application.validatorFactory(), - application.positionFactory()); + application, workflow, application.resourceLoaderFactory().getResourceLoader(path)); } public WorkflowInstance execute(Object input) { @@ -113,7 +94,7 @@ public Workflow workflow() { } public Collection listeners() { - return listeners; + return application.listeners(); } public Map> taskExecutors() { @@ -121,23 +102,27 @@ public Map> taskExecutors() { } public TaskExecutorFactory taskFactory() { - return taskFactory; + return application.taskFactory(); } public Optional outputFilter() { return outputFilter; } + public WorkflowIdFactory idFactory() { + return application.idFactory(); + } + public Optional outputSchemaValidator() { return outputSchemaValidator; } public ExpressionFactory expressionFactory() { - return exprFactory; + return application.expressionFactory(); } public SchemaValidatorFactory validatorFactory() { - return schemaValidatorFactory; + return application.validatorFactory(); } public ResourceLoader resourceLoader() { @@ -146,7 +131,11 @@ public ResourceLoader resourceLoader() { } public WorkflowPositionFactory positionFactory() { - return positionFactory; + return application.positionFactory(); + } + + public RuntimeDescriptorFactory runtimeDescriptorFactory() { + return application.runtimeDescriptorFactory(); } @Override diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowIdFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowIdFactory.java new file mode 100644 index 00000000..12b0f7c6 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowIdFactory.java @@ -0,0 +1,21 @@ +/* + * 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; + +import java.util.function.Supplier; + +@FunctionalInterface +public interface WorkflowIdFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index 1361c43f..444d3fd5 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -18,28 +18,54 @@ import static io.serverlessworkflow.impl.json.JsonUtils.toJavaValue; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import java.time.Instant; public class WorkflowInstance { private WorkflowState state; - private WorkflowContext context; private TaskContext taskContext; + private final String id; + private final JsonNode input; + private final Instant startedAt; + private JsonNode context = NullNode.getInstance(); WorkflowInstance(WorkflowDefinition definition, JsonNode input) { + this.id = definition.idFactory().get(); + this.input = input; definition.inputSchemaValidator().ifPresent(v -> v.validate(input)); - context = new WorkflowContext(definition, input); - taskContext = new TaskContext<>(input, definition.positionFactory().buildPosition()); + this.startedAt = Instant.now(); + WorkflowContext workflowContext = new WorkflowContext(definition, this); + taskContext = new TaskContext<>(input, definition.positionFactory().get()); definition .inputFilter() - .ifPresent(f -> taskContext.input(f.apply(context, taskContext, input))); + .ifPresent(f -> taskContext.input(f.apply(workflowContext, taskContext, input))); state = WorkflowState.STARTED; taskContext.rawOutput( - WorkflowUtils.processTaskList(definition.workflow().getDo(), context, taskContext)); + WorkflowUtils.processTaskList(definition.workflow().getDo(), workflowContext, taskContext)); definition .outputFilter() - .ifPresent(f -> taskContext.output(f.apply(context, taskContext, taskContext.rawOutput()))); + .ifPresent( + f -> + taskContext.output(f.apply(workflowContext, taskContext, taskContext.rawOutput()))); definition.outputSchemaValidator().ifPresent(v -> v.validate(taskContext.output())); } + public String id() { + return id; + } + + public Instant startedAt() { + return startedAt; + } + + public JsonNode input() { + return input; + } + + public JsonNode context() { + return context; + } + public WorkflowState state() { return state; } @@ -51,4 +77,8 @@ public Object output() { public Object outputAsJsonNode() { return taskContext.output(); } + + void context(JsonNode context) { + this.context = context; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java index cf63844a..1c416100 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java @@ -26,4 +26,6 @@ public interface WorkflowPosition { WorkflowPosition back(); WorkflowPosition copy(); + + Object last(); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java index e93a4c33..c2a3df7e 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java @@ -15,6 +15,6 @@ */ package io.serverlessworkflow.impl; -public interface WorkflowPositionFactory { - WorkflowPosition buildPosition(); -} +import java.util.function.Supplier; + +public interface WorkflowPositionFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/DateTimeDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/DateTimeDescriptor.java new file mode 100644 index 00000000..7936763f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/DateTimeDescriptor.java @@ -0,0 +1,48 @@ +/* + * 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.expressions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; + +public class DateTimeDescriptor { + + private final Instant instant; + + public static DateTimeDescriptor from(Instant instant) { + return new DateTimeDescriptor(instant); + } + + private DateTimeDescriptor(Instant instant) { + this.instant = instant; + } + + @JsonProperty("iso8601") + public String iso8601() { + return instant.toString(); + } + + @JsonProperty("epoch") + public Epoch epoch() { + return Epoch.of(instant); + } + + public static record Epoch(long seconds, long milliseconds) { + public static Epoch of(Instant instant) { + return new Epoch(instant.getEpochSecond(), instant.toEpochMilli()); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java index 9da21dbe..0207d3b5 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java @@ -31,26 +31,28 @@ public class JQExpression implements Expression { private final Supplier scope; private final String expr; - - private net.thisptr.jackson.jq.Expression internalExpr; + private final net.thisptr.jackson.jq.Expression internalExpr; public JQExpression(Supplier scope, String expr, Version version) throws JsonQueryException { this.expr = expr; this.scope = scope; - this.internalExpr = compile(version); - } - - private net.thisptr.jackson.jq.Expression compile(Version version) throws JsonQueryException { - return ExpressionParser.compile(expr, version); + this.internalExpr = ExpressionParser.compile(expr, version); } - private interface TypedOutput extends Output { - T getResult(); + @Override + public JsonNode eval(WorkflowContext workflow, TaskContext task, JsonNode node) { + JsonNodeOutput output = new JsonNodeOutput(); + try { + internalExpr.apply(createScope(workflow, task), node, output); + return output.getResult(); + } catch (JsonQueryException e) { + throw new IllegalArgumentException( + "Unable to evaluate content " + node + " using expr " + expr, e); + } } - private static class JsonNodeOutput implements TypedOutput { - + private static class JsonNodeOutput implements Output { private JsonNode result; private boolean arrayCreated; @@ -68,26 +70,21 @@ public void emit(JsonNode out) throws JsonQueryException { } } - @Override public JsonNode getResult() { return result; } } - @Override - public JsonNode eval(WorkflowContext workflow, TaskContext task, JsonNode node) { - TypedOutput output = new JsonNodeOutput(); - try { - internalExpr.apply(createScope(workflow, task), node, output); - return output.getResult(); - } catch (JsonQueryException e) { - throw new IllegalArgumentException( - "Unable to evaluate content " + node + " using expr " + expr, e); - } - } - private Scope createScope(WorkflowContext workflow, TaskContext task) { Scope childScope = Scope.newChildScope(scope.get()); + childScope.setValue("input", task.input()); + childScope.setValue("output", task.output()); + childScope.setValue("context", workflow.context()); + childScope.setValue( + "runtime", + () -> JsonUtils.fromValue(workflow.definition().runtimeDescriptorFactory().get())); + childScope.setValue("workflow", () -> JsonUtils.fromValue(WorkflowDescriptor.of(workflow))); + childScope.setValue("task", () -> JsonUtils.fromValue(TaskDescriptor.of(task))); task.variables().forEach((k, v) -> childScope.setValue(k, JsonUtils.fromValue(v))); return childScope; } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/RuntimeDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/RuntimeDescriptor.java new file mode 100644 index 00000000..66286632 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/RuntimeDescriptor.java @@ -0,0 +1,21 @@ +/* + * 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.expressions; + +import java.util.Map; + +public record RuntimeDescriptor(String name, String version, Map metadata) {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/TaskDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/TaskDescriptor.java new file mode 100644 index 00000000..a78bffa7 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/TaskDescriptor.java @@ -0,0 +1,39 @@ +/* + * 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.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; + +public record TaskDescriptor( + String name, + String reference, + T definition, + JsonNode rawInput, + JsonNode rawOutput, + DateTimeDescriptor startedAt) { + + public static TaskDescriptor of(TaskContext context) { + return new TaskDescriptor( + context.position().last().toString(), + context.position().jsonPointer(), + context.task(), + context.rawInput(), + context.rawOutput(), + DateTimeDescriptor.from(context.startedAt())); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/WorkflowDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/WorkflowDescriptor.java new file mode 100644 index 00000000..f6b906fb --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/WorkflowDescriptor.java @@ -0,0 +1,33 @@ +/* + * 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.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.WorkflowContext; + +public record WorkflowDescriptor( + String id, Workflow definition, JsonNode input, DateTimeDescriptor startedAt) { + + public static WorkflowDescriptor of(WorkflowContext context) { + return new WorkflowDescriptor( + context.instance().id(), + context.definition().workflow(), + context.instance().input(), + DateTimeDescriptor.from(context.instance().startedAt())); + } +} diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index 39adcf9d..27662797 100644 --- a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -19,10 +19,11 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; +import java.time.Instant; import java.util.Arrays; import java.util.Map; +import java.util.function.Consumer; import java.util.stream.Stream; -import org.assertj.core.api.Condition; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -31,36 +32,38 @@ public class WorkflowDefinitionTest { private static WorkflowApplication appl; + private static Instant before; @BeforeAll static void init() { appl = WorkflowApplication.builder().build(); + before = Instant.now(); } @ParameterizedTest @MethodSource("provideParameters") - void testWorkflowExecution(String fileName, Object input, Condition condition) + void testWorkflowExecution(String fileName, Object input, Consumer assertions) throws IOException { - assertThat(appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(input).output()) - .is(condition); + assertions.accept( + appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(input).output()); } private static Stream provideParameters() { return Stream.of( - Arguments.of( + args( "switch-then-string.yaml", Map.of("orderType", "electronic"), - new Condition( - o -> - o.equals( - Map.of("orderType", "electronic", "validate", true, "status", "fulfilled")), - "switch-electronic")), - Arguments.of( + o -> + assertThat(o) + .isEqualTo( + Map.of( + "orderType", "electronic", "validate", true, "status", "fulfilled"))), + args( "switch-then-string.yaml", Map.of("orderType", "physical"), - new Condition( - o -> - o.equals( + o -> + assertThat(o) + .isEqualTo( Map.of( "orderType", "physical", @@ -69,28 +72,48 @@ private static Stream provideParameters() { "items", 1, "address", - "Elmer St")), - "switch-physical")), - Arguments.of( + "Elmer St"))), + args( "switch-then-string.yaml", Map.of("orderType", "unknown"), - new Condition( - o -> - o.equals( + o -> + assertThat(o) + .isEqualTo( Map.of( - "orderType", "unknown", "log", "warn", "message", "something's wrong")), - "switch-unknown")), - Arguments.of( + "orderType", + "unknown", + "log", + "warn", + "message", + "something's wrong"))), + args( "for-sum.yaml", Map.of("input", Arrays.asList(1, 2, 3)), - new Condition(o -> o.equals(6), "for-sum")), - Arguments.of( + o -> assertThat(o).isEqualTo(6)), + args( "for-collect.yaml", Map.of("input", Arrays.asList(1, 2, 3)), - new Condition( - o -> - o.equals( - Map.of("input", Arrays.asList(1, 2, 3), "output", Arrays.asList(2, 4, 6))), - "for-collect"))); + o -> + assertThat(o) + .isEqualTo( + Map.of("input", Arrays.asList(1, 2, 3), "output", Arrays.asList(2, 4, 6)))), + args( + "simple-expression.yaml", + Map.of("input", Arrays.asList(1, 2, 3)), + WorkflowDefinitionTest::checkSpecialKeywords)); + } + + private static Arguments args( + String fileName, Map input, Consumer object) { + return Arguments.of(fileName, input, object); + } + + private static void checkSpecialKeywords(Object obj) { + Map result = (Map) obj; + assertThat(Instant.ofEpochMilli((long) result.get("startedAt"))) + .isAfterOrEqualTo(before) + .isBeforeOrEqualTo(Instant.now()); + assertThat(result.get("id").toString()).hasSize(26); + assertThat(result.get("version").toString()).contains("alpha"); } } diff --git a/impl/core/src/test/resources/simple-expression.yaml b/impl/core/src/test/resources/simple-expression.yaml new file mode 100644 index 00000000..4e240d6b --- /dev/null +++ b/impl/core/src/test/resources/simple-expression.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: simple-expression + version: '0.1.0' +do: + - useExpression: + set: + startedAt: ${$task.startedAt.epoch.milliseconds} + id : ${$workflow.id} + version: ${$runtime.version} \ No newline at end of file From 3bca1eeb3cf0e22471c97de024fff88a16b73fd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:23:29 -0500 Subject: [PATCH 17/22] Bump version.com.fasterxml.jackson from 2.18.1 to 2.18.2 (#487) Bumps `version.com.fasterxml.jackson` from 2.18.1 to 2.18.2. Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.1 to 2.18.2 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.1...jackson-core-2.18.2) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.18.1 to 2.18.2 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-yaml` from 2.18.1 to 2.18.2 - [Commits](https://github.com/FasterXML/jackson-dataformats-text/compare/jackson-dataformats-text-2.18.1...jackson-dataformats-text-2.18.2) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f6f3a593..471a1b45 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 1.5.12 - 2.18.1 + 2.18.2 1.5.4 3.1.0 1.5.2 From 493980e41b894b52e9ef24cbbaa35a34295d3a2f Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Mon, 2 Dec 2024 15:06:54 +0100 Subject: [PATCH 18/22] [Fix #468] Try/raise implementation Signed-off-by: Francisco Javier Tirado Sarti --- api/src/main/resources/schema/workflow.yaml | 30 ++++- .../serverlessworkflow/impl/StringFilter.java | 21 ++++ .../impl/WorkflowError.java | 73 ++++++++++++ .../impl/WorkflowException.java | 36 ++++++ .../impl/WorkflowInstance.java | 3 +- .../impl/WorkflowPositionFactory.java | 1 + .../impl/WorkflowUtils.java | 23 +++- .../executors/DefaultTaskExecutorFactory.java | 4 + .../impl/executors/DoExecutor.java | 2 +- .../impl/executors/ForExecutor.java | 2 +- .../impl/executors/RaiseExecutor.java | 104 ++++++++++++++++++ .../impl/executors/TryExecutor.java | 86 +++++++++++++++ .../impl/WorkflowDefinitionTest.java | 52 ++++++--- .../src/test/resources/raise-inline copy.yaml | 13 +++ .../src/test/resources/raise-reusable.yaml | 16 +++ .../impl/executors/HttpExecutor.java | 11 +- .../impl/HTTPWorkflowDefinitionTest.java | 5 + impl/http/src/test/resources/callGetHttp.yaml | 23 ++-- 18 files changed, 473 insertions(+), 32 deletions(-) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/StringFilter.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/RaiseExecutor.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java create mode 100644 impl/core/src/test/resources/raise-inline copy.yaml create mode 100644 impl/core/src/test/resources/raise-reusable.yaml diff --git a/api/src/main/resources/schema/workflow.yaml b/api/src/main/resources/schema/workflow.yaml index 42456ec6..aecbeacb 100644 --- a/api/src/main/resources/schema/workflow.yaml +++ b/api/src/main/resources/schema/workflow.yaml @@ -777,7 +777,10 @@ $defs: errors: type: object title: CatchErrors - description: The configuration of a concept used to catch errors. + properties: + with: + $ref: '#/$defs/errorFilter' + description: static error filter as: type: string title: CatchAs @@ -785,11 +788,11 @@ $defs: when: type: string title: CatchWhen - description: A runtime expression used to determine whether or not to catch the filtered error. + description: A runtime expression used to determine whether to catch the filtered error. exceptWhen: type: string title: CatchExceptWhen - description: A runtime expression used to determine whether or not to catch the filtered error. + description: A runtime expression used to determine whether not to catch the filtered error. retry: oneOf: - $ref: '#/$defs/retryPolicy' @@ -1152,6 +1155,27 @@ $defs: title: ErrorDetails description: A human-readable explanation specific to this occurrence of the error. required: [ type, status ] + errorFilter: + type: object + title: ErrorFilter + description: Error filtering base on static values. For error filtering on dynamic values, use catch.when property + minProperties: 1 + properties: + type: + type: string + description: if present, means this value should be used for filtering + status: + type: integer + description: if present, means this value should be used for filtering + instance: + type: string + description: if present, means this value should be used for filtering + title: + type: string + description: if present, means this value should be used for filtering + details: + type: string + description: if present, means this value should be used for filtering uriTemplate: title: UriTemplate anyOf: diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/StringFilter.java b/impl/core/src/main/java/io/serverlessworkflow/impl/StringFilter.java new file mode 100644 index 00000000..5d0a648e --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/StringFilter.java @@ -0,0 +1,21 @@ +/* + * 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; + +import java.util.function.BiFunction; + +@FunctionalInterface +public interface StringFilter extends BiFunction, String> {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java new file mode 100644 index 00000000..1823be94 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java @@ -0,0 +1,73 @@ +/* + * 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; + +public record WorkflowError( + String type, int status, String instance, String title, String details) { + + private static final String ERROR_FORMAT = "https://serverlessworkflow.io/spec/1.0.0/errors/%s"; + public static final String RUNTIME_TYPE = String.format(ERROR_FORMAT, "runtime"); + public static final String COMM_TYPE = String.format(ERROR_FORMAT, "communication"); + + public static Builder error(String type, int status) { + return new Builder(type, status); + } + + public static Builder communication(int status, TaskContext context, Exception ex) { + return new Builder(COMM_TYPE, status) + .instance(context.position().jsonPointer()) + .title(ex.getMessage()); + } + + public static Builder runtime(int status, TaskContext context, Exception ex) { + return new Builder(RUNTIME_TYPE, status) + .instance(context.position().jsonPointer()) + .title(ex.getMessage()); + } + + public static class Builder { + + private final String type; + private int status; + private String instance; + private String title; + private String details; + + private Builder(String type, int status) { + this.type = type; + this.status = status; + } + + public Builder instance(String instance) { + this.instance = instance; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + + public Builder details(String details) { + this.details = details; + return this; + } + + public WorkflowError build() { + return new WorkflowError(type, status, instance, title, details); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java new file mode 100644 index 00000000..685fc077 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java @@ -0,0 +1,36 @@ +/* + * 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; + +public class WorkflowException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final WorkflowError worflowError; + + public WorkflowException(WorkflowError error) { + this(error, null); + } + + public WorkflowException(WorkflowError error, Throwable cause) { + super(error.toString(), cause); + this.worflowError = error; + } + + public WorkflowError getWorflowError() { + return worflowError; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index 444d3fd5..0f3bd410 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -40,8 +40,7 @@ public class WorkflowInstance { .inputFilter() .ifPresent(f -> taskContext.input(f.apply(workflowContext, taskContext, input))); state = WorkflowState.STARTED; - taskContext.rawOutput( - WorkflowUtils.processTaskList(definition.workflow().getDo(), workflowContext, taskContext)); + WorkflowUtils.processTaskList(definition.workflow().getDo(), workflowContext, taskContext); definition .outputFilter() .ifPresent( diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java index c2a3df7e..60fa5d6a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java @@ -17,4 +17,5 @@ import java.util.function.Supplier; +@FunctionalInterface public interface WorkflowPositionFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java index 54abd7e7..01486d5f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -92,6 +92,23 @@ public static Optional buildWorkflowFilter( : Optional.empty(); } + public static StringFilter buildStringFilter( + ExpressionFactory exprFactory, String expression, String literal) { + return expression != null ? from(buildWorkflowFilter(exprFactory, expression)) : from(literal); + } + + public static StringFilter buildStringFilter(ExpressionFactory exprFactory, String str) { + return ExpressionUtils.isExpr(str) ? from(buildWorkflowFilter(exprFactory, str)) : from(str); + } + + public static StringFilter from(WorkflowFilter filter) { + return (w, t) -> filter.apply(w, t, t.input()).asText(); + } + + private static StringFilter from(String literal) { + return (w, t) -> literal; + } + private static WorkflowFilter buildWorkflowFilter( ExpressionFactory exprFactory, String str, Object object) { if (str != null) { @@ -127,7 +144,7 @@ private static TaskItem findTaskByName(ListIterator iter, String taskN throw new IllegalArgumentException("Cannot find task with name " + taskName); } - public static JsonNode processTaskList( + public static void processTaskList( List tasks, WorkflowContext context, TaskContext parentTask) { parentTask.position().addProperty("do"); TaskContext currentContext = parentTask; @@ -136,7 +153,7 @@ public static JsonNode processTaskList( TaskItem nextTask = iter.next(); while (nextTask != null) { TaskItem task = nextTask; - parentTask.position().addIndex(iter.nextIndex()).addProperty(task.getName()); + parentTask.position().addIndex(iter.previousIndex()).addProperty(task.getName()); context .definition() .listeners() @@ -175,7 +192,7 @@ public static JsonNode processTaskList( } } parentTask.position().back(); - return currentContext.output(); + parentTask.rawOutput(currentContext.output()); } public static WorkflowFilter buildWorkflowFilter(ExpressionFactory exprFactory, String str) { diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index 45a988a7..58bd18ac 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -67,6 +67,10 @@ public TaskExecutor getTaskExecutor( return new SetExecutor(task.getSetTask(), definition); } else if (task.getForTask() != null) { return new ForExecutor(task.getForTask(), definition); + } else if (task.getRaiseTask() != null) { + return new RaiseExecutor(task.getRaiseTask(), definition); + } else if (task.getTryTask() != null) { + return new TryExecutor(task.getTryTask(), definition); } throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java index df364c14..871a77da 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java @@ -29,6 +29,6 @@ protected DoExecutor(DoTask task, WorkflowDefinition definition) { @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { - taskContext.rawOutput(WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext)); + WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java index 511575f3..e74a18f9 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java @@ -52,7 +52,7 @@ protected void internalExecute(WorkflowContext workflow, TaskContext ta JsonNode item = iter.next(); taskContext.variables().put(task.getFor().getEach(), item); taskContext.variables().put(task.getFor().getAt(), i++); - taskContext.rawOutput(WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext)); + WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext); } } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RaiseExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RaiseExecutor.java new file mode 100644 index 00000000..1ddc315f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RaiseExecutor.java @@ -0,0 +1,104 @@ +/* + * 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.executors; + +import io.serverlessworkflow.api.types.Error; +import io.serverlessworkflow.api.types.ErrorInstance; +import io.serverlessworkflow.api.types.ErrorType; +import io.serverlessworkflow.api.types.RaiseTask; +import io.serverlessworkflow.api.types.RaiseTaskError; +import io.serverlessworkflow.impl.StringFilter; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; +import io.serverlessworkflow.impl.WorkflowUtils; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +public class RaiseExecutor extends AbstractTaskExecutor { + + private final BiFunction, WorkflowError> errorBuilder; + + private final StringFilter typeFilter; + private final Optional instanceFilter; + private final StringFilter titleFilter; + private final StringFilter detailFilter; + + protected RaiseExecutor(RaiseTask task, WorkflowDefinition definition) { + super(task, definition); + RaiseTaskError raiseError = task.getRaise().getError(); + Error error = + raiseError.getRaiseErrorDefinition() != null + ? raiseError.getRaiseErrorDefinition() + : findError(definition, raiseError.getRaiseErrorReference()); + this.typeFilter = getTypeFunction(definition.expressionFactory(), error.getType()); + this.instanceFilter = getInstanceFunction(definition.expressionFactory(), error.getInstance()); + this.titleFilter = + WorkflowUtils.buildStringFilter(definition.expressionFactory(), error.getTitle()); + this.detailFilter = + WorkflowUtils.buildStringFilter(definition.expressionFactory(), error.getDetail()); + this.errorBuilder = (w, t) -> buildError(error, w, t); + } + + private static Error findError(WorkflowDefinition definition, String raiseErrorReference) { + Map errorsMap = + definition.workflow().getUse().getErrors().getAdditionalProperties(); + Error error = errorsMap.get(raiseErrorReference); + if (error == null) { + throw new IllegalArgumentException("Error " + error + "is not defined in " + errorsMap); + } + return error; + } + + private WorkflowError buildError( + Error error, WorkflowContext context, TaskContext taskContext) { + return WorkflowError.error(typeFilter.apply(context, taskContext), error.getStatus()) + .instance( + instanceFilter + .map(f -> f.apply(context, taskContext)) + .orElseGet(() -> taskContext.position().jsonPointer())) + .title(titleFilter.apply(context, taskContext)) + .details(detailFilter.apply(context, taskContext)) + .build(); + } + + private Optional getInstanceFunction( + ExpressionFactory expressionFactory, ErrorInstance errorInstance) { + return errorInstance != null + ? Optional.of( + WorkflowUtils.buildStringFilter( + expressionFactory, + errorInstance.getExpressionErrorInstance(), + errorInstance.getLiteralErrorInstance())) + : Optional.empty(); + } + + private StringFilter getTypeFunction(ExpressionFactory expressionFactory, ErrorType type) { + return WorkflowUtils.buildStringFilter( + expressionFactory, + type.getExpressionErrorType(), + type.getLiteralErrorType().get().toString()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + throw new WorkflowException(errorBuilder.apply(workflow, taskContext)); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java new file mode 100644 index 00000000..67af7995 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java @@ -0,0 +1,86 @@ +/* + * 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.executors; + +import io.serverlessworkflow.api.types.CatchErrors; +import io.serverlessworkflow.api.types.ErrorFilter; +import io.serverlessworkflow.api.types.TryTask; +import io.serverlessworkflow.api.types.TryTaskCatch; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowUtils; +import java.util.Optional; +import java.util.function.Predicate; + +public class TryExecutor extends AbstractTaskExecutor { + + private final Optional whenFilter; + private final Optional exceptFilter; + private final Optional> errorFilter; + + protected TryExecutor(TryTask task, WorkflowDefinition definition) { + super(task, definition); + TryTaskCatch catchInfo = task.getCatch(); + this.errorFilter = buildErrorFilter(catchInfo.getErrors()); + this.whenFilter = + WorkflowUtils.optionalFilter(definition.expressionFactory(), catchInfo.getWhen()); + this.exceptFilter = + WorkflowUtils.optionalFilter(definition.expressionFactory(), catchInfo.getExceptWhen()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + try { + WorkflowUtils.processTaskList(task.getTry(), workflow, taskContext); + } catch (WorkflowException exception) { + if (errorFilter.map(f -> f.test(exception.getWorflowError())).orElse(true) + && whenFilter + .map(w -> w.apply(workflow, taskContext, taskContext.input()).asBoolean()) + .orElse(true) + && exceptFilter + .map(w -> !w.apply(workflow, taskContext, taskContext.input()).asBoolean()) + .orElse(true)) { + if (task.getCatch().getDo() != null) { + WorkflowUtils.processTaskList(task.getCatch().getDo(), workflow, taskContext); + } + } else { + throw exception; + } + } + } + + private static Optional> buildErrorFilter(CatchErrors errors) { + return errors != null + ? Optional.of(error -> filterError(error, errors.getWith())) + : Optional.empty(); + } + + private static boolean filterError(WorkflowError error, ErrorFilter errorFilter) { + return compareString(errorFilter.getType(), error.type()) + && (errorFilter.getStatus() <= 0 || error.status() == errorFilter.getStatus()) + && compareString(errorFilter.getInstance(), error.instance()) + && compareString(errorFilter.getTitle(), error.title()) + && compareString(errorFilter.getDetails(), errorFilter.getDetails()); + } + + private static boolean compareString(String one, String other) { + return one == null || one.equals(other); + } +} diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index 27662797..6b29bac1 100644 --- a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -17,6 +17,7 @@ import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; import java.io.IOException; import java.time.Instant; @@ -42,10 +43,9 @@ static void init() { @ParameterizedTest @MethodSource("provideParameters") - void testWorkflowExecution(String fileName, Object input, Consumer assertions) + void testWorkflowExecution(String fileName, Consumer assertions) throws IOException { - assertions.accept( - appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(input).output()); + assertions.accept(appl.workflowDefinition(readWorkflowFromClasspath(fileName))); } private static Stream provideParameters() { @@ -54,7 +54,7 @@ private static Stream provideParameters() { "switch-then-string.yaml", Map.of("orderType", "electronic"), o -> - assertThat(o) + assertThat(o.output()) .isEqualTo( Map.of( "orderType", "electronic", "validate", true, "status", "fulfilled"))), @@ -62,7 +62,7 @@ private static Stream provideParameters() { "switch-then-string.yaml", Map.of("orderType", "physical"), o -> - assertThat(o) + assertThat(o.output()) .isEqualTo( Map.of( "orderType", @@ -77,7 +77,7 @@ private static Stream provideParameters() { "switch-then-string.yaml", Map.of("orderType", "unknown"), o -> - assertThat(o) + assertThat(o.output()) .isEqualTo( Map.of( "orderType", @@ -89,27 +89,53 @@ private static Stream provideParameters() { args( "for-sum.yaml", Map.of("input", Arrays.asList(1, 2, 3)), - o -> assertThat(o).isEqualTo(6)), + o -> assertThat(o.output()).isEqualTo(6)), args( "for-collect.yaml", Map.of("input", Arrays.asList(1, 2, 3)), o -> - assertThat(o) + assertThat(o.output()) .isEqualTo( Map.of("input", Arrays.asList(1, 2, 3), "output", Arrays.asList(2, 4, 6)))), args( "simple-expression.yaml", Map.of("input", Arrays.asList(1, 2, 3)), - WorkflowDefinitionTest::checkSpecialKeywords)); + WorkflowDefinitionTest::checkSpecialKeywords), + args( + "raise-inline copy.yaml", + WorkflowDefinitionTest::checkWorkflowException, + WorkflowException.class), + args( + "raise-reusable.yaml", + WorkflowDefinitionTest::checkWorkflowException, + WorkflowException.class)); } private static Arguments args( - String fileName, Map input, Consumer object) { - return Arguments.of(fileName, input, object); + String fileName, Map input, Consumer instance) { + return Arguments.of( + fileName, (Consumer) d -> instance.accept(d.execute(input))); + } + + private static Arguments args( + String fileName, Consumer consumer, Class clazz) { + return Arguments.of( + fileName, + (Consumer) + d -> consumer.accept(catchThrowableOfType(clazz, () -> d.execute(Map.of())))); + } + + private static void checkWorkflowException(WorkflowException ex) { + assertThat(ex.getWorflowError().type()) + .isEqualTo("https://serverlessworkflow.io/errors/not-implemented"); + assertThat(ex.getWorflowError().status()).isEqualTo(500); + assertThat(ex.getWorflowError().title()).isEqualTo("Not Implemented"); + assertThat(ex.getWorflowError().details()).contains("raise-not-implemented"); + assertThat(ex.getWorflowError().instance()).isEqualTo("do/0/notImplemented"); } - private static void checkSpecialKeywords(Object obj) { - Map result = (Map) obj; + private static void checkSpecialKeywords(WorkflowInstance obj) { + Map result = (Map) obj.output(); assertThat(Instant.ofEpochMilli((long) result.get("startedAt"))) .isAfterOrEqualTo(before) .isBeforeOrEqualTo(Instant.now()); diff --git a/impl/core/src/test/resources/raise-inline copy.yaml b/impl/core/src/test/resources/raise-inline copy.yaml new file mode 100644 index 00000000..b4bcac88 --- /dev/null +++ b/impl/core/src/test/resources/raise-inline copy.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: raise-not-implemented + version: '0.1.0' +do: + - notImplemented: + raise: + error: + type: https://serverlessworkflow.io/errors/not-implemented + status: 500 + title: Not Implemented + detail: ${ "The workflow '\( $workflow.definition.document.name ):\( $workflow.definition.document.version )' is a work in progress and cannot be run yet" } \ No newline at end of file diff --git a/impl/core/src/test/resources/raise-reusable.yaml b/impl/core/src/test/resources/raise-reusable.yaml new file mode 100644 index 00000000..6955bd05 --- /dev/null +++ b/impl/core/src/test/resources/raise-reusable.yaml @@ -0,0 +1,16 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: raise-not-implemented-reusable + version: '0.1.0' +use: + errors: + notImplemented: + type: https://serverlessworkflow.io/errors/not-implemented + status: 500 + title: Not Implemented + detail: ${ "The workflow '\( $workflow.definition.document.name ):\( $workflow.definition.document.version )' is a work in progress and cannot be run yet" } +do: + - notImplemented: + raise: + error: notImplemented \ No newline at end of file diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index 5ecd27af..13e61d35 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -26,11 +26,14 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; import io.serverlessworkflow.impl.expressions.Expression; import io.serverlessworkflow.impl.expressions.ExpressionFactory; import io.serverlessworkflow.impl.expressions.ExpressionUtils; import io.serverlessworkflow.impl.json.JsonUtils; import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Entity; @@ -101,7 +104,13 @@ public JsonNode apply( Builder request = target.request(); ExpressionUtils.evaluateExpressionMap(headersMap, workflow, taskContext, input) .forEach(request::header); - return requestFunction.apply(request, workflow, taskContext, input); + try { + return requestFunction.apply(request, workflow, taskContext, input); + } catch (WebApplicationException exception) { + throw new WorkflowException( + WorkflowError.communication(exception.getResponse().getStatus(), taskContext, exception) + .build()); + } } @Override diff --git a/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java index dacdfe2e..f3d77bdd 100644 --- a/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java +++ b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java @@ -69,6 +69,11 @@ private static Stream provideParameters() { o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"); return Stream.of( Arguments.of("callGetHttp.yaml", petInput, petCondition), + Arguments.of( + "callGetHttp.yaml", + Map.of("petId", "-1"), + new Condition<>( + o -> ((Map) o).containsKey("petId"), "notFoundCondition")), Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition), Arguments.of( "call-http-query-parameters.yaml", diff --git a/impl/http/src/test/resources/callGetHttp.yaml b/impl/http/src/test/resources/callGetHttp.yaml index 6fc07807..192b0bcd 100644 --- a/impl/http/src/test/resources/callGetHttp.yaml +++ b/impl/http/src/test/resources/callGetHttp.yaml @@ -4,11 +4,18 @@ document: name: http-call-with-response version: 1.0.0 do: - - getPet: - call: http - with: - headers: - content-type: application/json - method: get - endpoint: - uri: https://petstore.swagger.io/v2/pet/{petId} \ No newline at end of file + - tryGetPet: + try: + - getPet: + call: http + with: + headers: + content-type: application/json + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + catch: + errors: + with: + type: https://serverlessworkflow.io/spec/1.0.0/errors/communication + status: 404 \ No newline at end of file From 74fc9589d8df501d7de784db28362963e440aac0 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Mon, 2 Dec 2024 21:24:37 +0100 Subject: [PATCH 19/22] [Fix #484] Execute Fork task Signed-off-by: Francisco Javier Tirado Sarti --- .../impl/ExecutorServiceFactory.java | 22 ++++ .../impl/QueueWorkflowPosition.java | 4 +- .../serverlessworkflow/impl/TaskContext.java | 71 ++++++++--- .../impl/WorkflowApplication.java | 23 ++++ .../impl/WorkflowDefinition.java | 5 + .../impl/WorkflowExecutionListener.java | 6 +- .../impl/WorkflowInstance.java | 22 ++-- .../impl/WorkflowUtils.java | 43 +++---- .../impl/executors/AbstractTaskExecutor.java | 15 +++ .../executors/DefaultTaskExecutorFactory.java | 2 + .../impl/executors/ForkExecutor.java | 114 ++++++++++++++++++ .../impl/generic/SortedArrayList.java | 67 ++++++++++ .../impl/SortedListTest.java | 64 ++++++++++ .../impl/WorkflowDefinitionTest.java | 31 ++++- .../src/test/resources/fork-no-compete.yaml | 18 +++ impl/core/src/test/resources/fork.yaml | 18 +++ 16 files changed, 473 insertions(+), 52 deletions(-) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/ExecutorServiceFactory.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java create mode 100644 impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java create mode 100644 impl/core/src/test/resources/fork-no-compete.yaml create mode 100644 impl/core/src/test/resources/fork.yaml diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/ExecutorServiceFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/ExecutorServiceFactory.java new file mode 100644 index 00000000..7c211149 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/ExecutorServiceFactory.java @@ -0,0 +1,22 @@ +/* + * 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; + +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +@FunctionalInterface +public interface ExecutorServiceFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java index c6d3f141..5ad4934f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java @@ -54,7 +54,7 @@ public String jsonPointer() { @Override public String toString() { - return "ListWorkflowPosition [list=" + queue + "]"; + return "QueueWorkflowPosition [queue=" + queue + "]"; } @Override @@ -65,6 +65,6 @@ public WorkflowPosition back() { @Override public Object last() { - return queue.pollLast(); + return queue.getLast(); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java index cadde89c..8015c687 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -28,35 +28,64 @@ public class TaskContext { private final JsonNode rawInput; private final T task; private final WorkflowPosition position; - private final Instant startedAt = Instant.now(); + private final Instant startedAt; private JsonNode input; private JsonNode output; private JsonNode rawOutput; private FlowDirective flowDirective; private Map contextVariables; + private Instant completedAt; public TaskContext(JsonNode input, WorkflowPosition position) { - this.rawInput = input; - this.position = position; - this.task = null; - this.contextVariables = new HashMap<>(); - init(); + this(input, null, position, Instant.now(), input, input, input, null, new HashMap<>()); } - public TaskContext(JsonNode input, TaskContext taskContext, T task) { - this.rawInput = input; - this.position = taskContext.position.copy(); - this.task = task; - this.flowDirective = task.getThen(); - this.contextVariables = new HashMap<>(taskContext.variables()); - init(); + public TaskContext copy() { + return new TaskContext( + rawInput, + task, + position.copy(), + startedAt, + input, + output, + rawOutput, + flowDirective, + new HashMap<>(contextVariables)); } - private void init() { - this.input = rawInput; - this.rawOutput = rawInput; - this.output = rawInput; + public TaskContext(JsonNode input, TaskContext taskContext, T task) { + this( + input, + task, + taskContext.position, + Instant.now(), + input, + input, + input, + task.getThen(), + new HashMap<>(taskContext.variables())); + } + + private TaskContext( + JsonNode rawInput, + T task, + WorkflowPosition position, + Instant startedAt, + JsonNode input, + JsonNode output, + JsonNode rawOutput, + FlowDirective flowDirective, + Map contextVariables) { + this.rawInput = rawInput; + this.task = task; + this.position = position; + this.startedAt = startedAt; + this.input = input; + this.output = output; + this.rawOutput = rawOutput; + this.flowDirective = flowDirective; + this.contextVariables = contextVariables; } public void input(JsonNode input) { @@ -115,4 +144,12 @@ public WorkflowPosition position() { public Instant startedAt() { return startedAt; } + + public void completedAt(Instant instant) { + this.completedAt = instant; + } + + public Instant completedAt() { + return completedAt; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java index 4f35de41..f36c23f6 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java @@ -32,6 +32,8 @@ import java.util.HashSet; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class WorkflowApplication implements AutoCloseable { @@ -43,8 +45,11 @@ public class WorkflowApplication implements AutoCloseable { private final Collection listeners; private final Map definitions; private final WorkflowPositionFactory positionFactory; + private final ExecutorServiceFactory executorFactory; private final RuntimeDescriptorFactory runtimeDescriptorFactory; + private ExecutorService executorService; + public WorkflowApplication( TaskExecutorFactory taskFactory, ExpressionFactory exprFactory, @@ -53,6 +58,7 @@ public WorkflowApplication( WorkflowPositionFactory positionFactory, WorkflowIdFactory idFactory, RuntimeDescriptorFactory runtimeDescriptorFactory, + ExecutorServiceFactory executorFactory, Collection listeners) { this.taskFactory = taskFactory; this.exprFactory = exprFactory; @@ -61,6 +67,7 @@ public WorkflowApplication( this.positionFactory = positionFactory; this.idFactory = idFactory; this.runtimeDescriptorFactory = runtimeDescriptorFactory; + this.executorFactory = executorFactory; this.listeners = listeners; this.definitions = new ConcurrentHashMap<>(); } @@ -101,6 +108,7 @@ public static class Builder { private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get(); private WorkflowPositionFactory positionFactory = () -> new QueueWorkflowPosition(); private WorkflowIdFactory idFactory = () -> UlidCreator.getMonotonicUlid().toString(); + private ExecutorServiceFactory executorFactory = () -> Executors.newCachedThreadPool(); private RuntimeDescriptorFactory descriptorFactory = () -> new RuntimeDescriptor("reference impl", "1.0.0_alpha", Collections.emptyMap()); @@ -129,6 +137,11 @@ public Builder withResourceLoaderFactory(ResourceLoaderFactory resourceLoader) { return this; } + public Builder withExecutorFactory(ExecutorServiceFactory executorFactory) { + this.executorFactory = executorFactory; + return this; + } + public Builder withPositionFactory(WorkflowPositionFactory positionFactory) { this.positionFactory = positionFactory; return this; @@ -158,6 +171,7 @@ public WorkflowApplication build() { positionFactory, idFactory, descriptorFactory, + executorFactory, listeners == null ? Collections.emptySet() : Collections.unmodifiableCollection(listeners)); @@ -190,4 +204,13 @@ public WorkflowPositionFactory positionFactory() { public RuntimeDescriptorFactory runtimeDescriptorFactory() { return runtimeDescriptorFactory; } + + public ExecutorService executorService() { + synchronized (executorFactory) { + if (executorService == null) { + executorService = executorFactory.get(); + } + } + return executorService; + } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index db8b5e4d..39d03809 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; public class WorkflowDefinition implements AutoCloseable { @@ -134,6 +135,10 @@ public WorkflowPositionFactory positionFactory() { return application.positionFactory(); } + public ExecutorService executorService() { + return application.executorService(); + } + public RuntimeDescriptorFactory runtimeDescriptorFactory() { return application.runtimeDescriptorFactory(); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java index ce72c70e..c121bb41 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java @@ -15,11 +15,11 @@ */ package io.serverlessworkflow.impl; -import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskBase; public interface WorkflowExecutionListener { - void onTaskStarted(WorkflowPosition currentPos, Task task); + void onTaskStarted(WorkflowPosition currentPos, TaskBase task); - void onTaskEnded(WorkflowPosition currentPos, Task task); + void onTaskEnded(WorkflowPosition currentPos, TaskBase task); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index 0f3bd410..8225439f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -20,14 +20,15 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.NullNode; import java.time.Instant; +import java.util.concurrent.atomic.AtomicReference; public class WorkflowInstance { - private WorkflowState state; - private TaskContext taskContext; + private final AtomicReference state; + private final TaskContext taskContext; private final String id; private final JsonNode input; private final Instant startedAt; - private JsonNode context = NullNode.getInstance(); + private final AtomicReference context; WorkflowInstance(WorkflowDefinition definition, JsonNode input) { this.id = definition.idFactory().get(); @@ -39,7 +40,8 @@ public class WorkflowInstance { definition .inputFilter() .ifPresent(f -> taskContext.input(f.apply(workflowContext, taskContext, input))); - state = WorkflowState.STARTED; + state = new AtomicReference<>(WorkflowState.STARTED); + context = new AtomicReference<>(NullNode.getInstance()); WorkflowUtils.processTaskList(definition.workflow().getDo(), workflowContext, taskContext); definition .outputFilter() @@ -62,22 +64,26 @@ public JsonNode input() { } public JsonNode context() { - return context; + return context.get(); } public WorkflowState state() { - return state; + return state.get(); + } + + public void state(WorkflowState state) { + this.state.set(state); } public Object output() { return toJavaValue(taskContext.output()); } - public Object outputAsJsonNode() { + public JsonNode outputAsJsonNode() { return taskContext.output(); } void context(JsonNode context) { - this.context = context; + this.context.set(context); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java index 01486d5f..56b3499a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -153,23 +153,8 @@ public static void processTaskList( TaskItem nextTask = iter.next(); while (nextTask != null) { TaskItem task = nextTask; - parentTask.position().addIndex(iter.previousIndex()).addProperty(task.getName()); - context - .definition() - .listeners() - .forEach(l -> l.onTaskStarted(parentTask.position(), task.getTask())); - currentContext = - context - .definition() - .taskExecutors() - .computeIfAbsent( - parentTask.position().jsonPointer(), - k -> - context - .definition() - .taskFactory() - .getTaskExecutor(task.getTask(), context.definition())) - .apply(context, parentTask, currentContext.output()); + parentTask.position().addIndex(iter.previousIndex()); + currentContext = executeTask(context, parentTask, task, currentContext.output()); FlowDirective flowDirective = currentContext.flowDirective(); if (flowDirective.getFlowDirectiveEnum() != null) { switch (flowDirective.getFlowDirectiveEnum()) { @@ -177,6 +162,7 @@ public static void processTaskList( nextTask = iter.hasNext() ? iter.next() : null; break; case END: + context.instance().state(WorkflowState.COMPLETED); case EXIT: nextTask = null; break; @@ -184,10 +170,6 @@ public static void processTaskList( } else { nextTask = WorkflowUtils.findTaskByName(iter, flowDirective.getString()); } - context - .definition() - .listeners() - .forEach(l -> l.onTaskEnded(parentTask.position(), task.getTask())); parentTask.position().back(); } } @@ -195,6 +177,25 @@ public static void processTaskList( parentTask.rawOutput(currentContext.output()); } + public static TaskContext executeTask( + WorkflowContext context, TaskContext parentTask, TaskItem task, JsonNode input) { + parentTask.position().addProperty(task.getName()); + TaskContext result = + context + .definition() + .taskExecutors() + .computeIfAbsent( + parentTask.position().jsonPointer(), + k -> + context + .definition() + .taskFactory() + .getTaskExecutor(task.getTask(), context.definition())) + .apply(context, parentTask, input); + parentTask.position().back(); + return result; + } + public static WorkflowFilter buildWorkflowFilter(ExpressionFactory exprFactory, String str) { assert str != null; Expression expression = exprFactory.getExpression(str); diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java index 086c7c51..44c6a7fd 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -26,7 +26,9 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowState; import io.serverlessworkflow.impl.jsonschema.SchemaValidator; +import java.time.Instant; import java.util.Optional; public abstract class AbstractTaskExecutor implements TaskExecutor { @@ -86,6 +88,14 @@ private void buildContextProcessors(WorkflowDefinition definition) { public TaskContext apply( WorkflowContext workflowContext, TaskContext parentContext, JsonNode input) { TaskContext taskContext = new TaskContext<>(input, parentContext, task); + if (workflowContext.instance().state() == WorkflowState.COMPLETED) { + return taskContext; + } + workflowContext + .definition() + .listeners() + .forEach(l -> l.onTaskStarted(parentContext.position(), task)); + inputSchemaValidator.ifPresent(s -> s.validate(taskContext.rawInput())); inputProcessor.ifPresent( p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); @@ -98,6 +108,11 @@ public TaskContext apply( workflowContext.context( p.apply(workflowContext, taskContext, workflowContext.context()))); contextSchemaValidator.ifPresent(s -> s.validate(workflowContext.context())); + taskContext.completedAt(Instant.now()); + workflowContext + .definition() + .listeners() + .forEach(l -> l.onTaskEnded(parentContext.position(), task)); return taskContext; } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index 58bd18ac..89c099b6 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -71,6 +71,8 @@ public TaskExecutor getTaskExecutor( return new RaiseExecutor(task.getRaiseTask(), definition); } else if (task.getTryTask() != null) { return new TryExecutor(task.getTryTask(), definition); + } else if (task.getForkTask() != null) { + return new ForkExecutor(task.getForkTask(), definition); } throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java new file mode 100644 index 00000000..484a6b9f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java @@ -0,0 +1,114 @@ +/* + * 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.executors; + +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.api.types.ForkTask; +import io.serverlessworkflow.api.types.ForkTaskConfiguration; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowState; +import io.serverlessworkflow.impl.WorkflowUtils; +import io.serverlessworkflow.impl.generic.SortedArrayList; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ForkExecutor extends AbstractTaskExecutor { + + private static final Logger logger = LoggerFactory.getLogger(ForkExecutor.class); + private final ExecutorService service; + + protected ForkExecutor(ForkTask task, WorkflowDefinition definition) { + super(task, definition); + service = definition.executorService(); + } + + private record BranchContext(String taskName, TaskContext taskContext) {} + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + ForkTaskConfiguration forkConfig = task.getFork(); + + if (!forkConfig.getBranches().isEmpty()) { + Map>> futures = new HashMap<>(); + int index = 0; + for (TaskItem item : forkConfig.getBranches()) { + final int i = index++; + futures.put( + item.getName(), + service.submit(() -> executeBranch(workflow, taskContext.copy(), item, i))); + } + List results = + new SortedArrayList<>( + (arg1, arg2) -> + arg1.taskContext.completedAt().compareTo(arg2.taskContext.completedAt())); + for (Map.Entry>> entry : futures.entrySet()) { + try { + results.add(new BranchContext(entry.getKey(), entry.getValue().get())); + } catch (ExecutionException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new UndeclaredThrowableException(ex); + } + } catch (InterruptedException ex) { + logger.warn( + "Thred executing branch {} was interrupted, this branch will be ignored", + entry.getKey(), + ex); + } + } + if (!results.isEmpty()) { + taskContext.rawOutput( + forkConfig.isCompete() + ? results.get(0).taskContext().output() + : JsonUtils.fromValue( + results.stream() + .map( + e -> + JsonUtils.mapper() + .createObjectNode() + .set(e.taskName(), e.taskContext().output())) + .collect(Collectors.toList()))); + } + } + } + + private TaskContext executeBranch( + WorkflowContext workflow, TaskContext taskContext, TaskItem taskItem, int index) { + taskContext.position().addIndex(index); + TaskContext result = + WorkflowUtils.executeTask(workflow, taskContext, taskItem, taskContext.input()); + if (result.flowDirective() != null + && result.flowDirective().getFlowDirectiveEnum() == FlowDirectiveEnum.END) { + workflow.instance().state(WorkflowState.COMPLETED); + } + taskContext.position().back(); + return result; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java b/impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java new file mode 100644 index 00000000..e0647d1f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java @@ -0,0 +1,67 @@ +/* + * 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.generic; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; + +public class SortedArrayList extends ArrayList { + + private static final long serialVersionUID = 1L; + private final Comparator comparator; + + public SortedArrayList() { + this(SortedArrayList::defaultCompare); + } + + @SuppressWarnings("unchecked") + private static int defaultCompare(V a, V b) { + return a instanceof Comparable ? ((Comparable) a).compareTo(b) : 0; + } + + public SortedArrayList(Comparator comparator) { + this.comparator = comparator; + } + + public SortedArrayList(Collection collection) { + this(collection, SortedArrayList::defaultCompare); + } + + public SortedArrayList(Collection collection, Comparator comparator) { + super(collection.size()); + this.comparator = comparator; + addAll(collection); + } + + @Override + public boolean add(T object) { + int i; + for (i = 0; i < size() && comparator.compare(object, get(i)) >= 0; i++) {} + super.add(i, object); + return true; + } + + public boolean addAll(Collection c) { + ensureCapacity(size() + c.size()); + c.forEach(this::add); + return !c.isEmpty(); + } + + public T set(int index, T element) { + throw new UnsupportedOperationException("Do not allow adding in a particular index"); + } +} diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java new file mode 100644 index 00000000..1ad0ed98 --- /dev/null +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java @@ -0,0 +1,64 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.impl.generic.SortedArrayList; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class SortedListTest { + + private record Person(String name, int age) {} + + @Test + void testConstructor() { + assertThat(new SortedArrayList<>(Arrays.asList(3, 2, 1))).isEqualTo(Arrays.asList(1, 2, 3)); + } + + @Test + void testAdd() { + List list = new SortedArrayList<>(); + list.add(1); + list.add(4); + list.add(3); + list.add(2); + assertThat(list).isEqualTo(Arrays.asList(1, 2, 3, 4)); + } + + @Test + void testAddPojo() { + List list = new SortedArrayList<>((a, b) -> b.age() - a.age()); + list.add(new Person("Mariam", 5)); + list.add(new Person("Belen", 12)); + list.add(new Person("Alejandro", 7)); + list.add(new Person("Vicente", 16)); + list.add(new Person("Daniel", 14)); + assertThat(list.stream().map(Person::name)) + .isEqualTo(Arrays.asList("Vicente", "Daniel", "Belen", "Alejandro", "Mariam")); + } + + @Test + void testAddAll() { + List list = new SortedArrayList<>(); + Instant now = Instant.now(); + list.addAll(Arrays.asList(now.plusMillis(1000), now)); + assertThat(list.get(0)).isEqualTo(now); + } +} diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index 6b29bac1..e324de2a 100644 --- a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -19,6 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowableOfType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.serverlessworkflow.impl.json.JsonUtils; import java.io.IOException; import java.time.Instant; import java.util.Arrays; @@ -108,7 +112,14 @@ private static Stream provideParameters() { args( "raise-reusable.yaml", WorkflowDefinitionTest::checkWorkflowException, - WorkflowException.class)); + WorkflowException.class), + args( + "fork.yaml", + Map.of(), + o -> + assertThat(((ObjectNode) o.outputAsJsonNode()).get("patientId").asText()) + .isIn("John", "Smith")), + args("fork-no-compete.yaml", Map.of(), WorkflowDefinitionTest::checkNotCompeteOuput)); } private static Arguments args( @@ -125,6 +136,24 @@ private static Arguments args( d -> consumer.accept(catchThrowableOfType(clazz, () -> d.execute(Map.of())))); } + private static void checkNotCompeteOuput(WorkflowInstance instance) { + JsonNode out = instance.outputAsJsonNode(); + assertThat(out).isInstanceOf(ArrayNode.class); + assertThat(out).hasSize(2); + ArrayNode array = (ArrayNode) out; + assertThat(array) + .containsExactlyInAnyOrder( + createObjectNode("callNurse", "patientId", "John", "room", 1), + createObjectNode("callDoctor", "patientId", "Smith", "room", 2)); + } + + private static JsonNode createObjectNode( + String parent, String key1, String value1, String key2, int value2) { + return JsonUtils.mapper() + .createObjectNode() + .set(parent, JsonUtils.mapper().createObjectNode().put(key1, value1).put(key2, value2)); + } + private static void checkWorkflowException(WorkflowException ex) { assertThat(ex.getWorflowError().type()) .isEqualTo("https://serverlessworkflow.io/errors/not-implemented"); diff --git a/impl/core/src/test/resources/fork-no-compete.yaml b/impl/core/src/test/resources/fork-no-compete.yaml new file mode 100644 index 00000000..5e78acf2 --- /dev/null +++ b/impl/core/src/test/resources/fork-no-compete.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: fork-example + version: '0.1.0' +do: + - callSomeone: + fork: + compete: false + branches: + - callNurse: + set: + patientId: John + room: 1 + - callDoctor: + set: + patientId: Smith + room: 2 \ No newline at end of file diff --git a/impl/core/src/test/resources/fork.yaml b/impl/core/src/test/resources/fork.yaml new file mode 100644 index 00000000..dfde183b --- /dev/null +++ b/impl/core/src/test/resources/fork.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: fork-no-compete + version: '0.1.0' +do: + - callSomeone: + fork: + compete: true + branches: + - callNurse: + set: + patientId: John + room: 1 + - callDoctor: + set: + patientId: Smith + room: 2 \ No newline at end of file From 202d485116c2b8a61b7dfc055d0addfd3bdd6367 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Tue, 3 Dec 2024 19:20:47 +0100 Subject: [PATCH 20/22] [Fix #484] Status related changes Signed-off-by: Francisco Javier Tirado Sarti --- impl/core/pom.xml | 5 + .../serverlessworkflow/impl/LongFilter.java | 21 ++++ .../serverlessworkflow/impl/TaskContext.java | 26 ++-- .../impl/WorkflowDefinition.java | 7 +- .../impl/WorkflowInstance.java | 16 +-- ...WorkflowState.java => WorkflowStatus.java} | 9 +- .../impl/WorkflowUtils.java | 99 ++++------------ .../impl/executors/AbstractTaskExecutor.java | 58 +++++---- .../executors/DefaultTaskExecutorFactory.java | 2 + .../impl/executors/DoExecutor.java | 3 +- .../impl/executors/ForExecutor.java | 2 +- .../impl/executors/ForkExecutor.java | 48 ++++---- .../impl/executors/TaskExecutorHelper.java | 111 ++++++++++++++++++ .../impl/executors/TryExecutor.java | 4 +- .../impl/executors/WaitExecutor.java | 58 +++++++++ .../impl/generic/SortedArrayList.java | 67 ----------- .../impl/json/JsonUtils.java | 39 ++++++ .../jsonschema/DefaultSchemaValidator.java | 17 +-- .../impl/SortedListTest.java | 64 ---------- .../impl/WorkflowDefinitionTest.java | 4 + .../src/test/resources/fork-no-compete.yaml | 22 +++- impl/core/src/test/resources/fork.yaml | 2 +- 22 files changed, 364 insertions(+), 320 deletions(-) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/LongFilter.java rename impl/core/src/main/java/io/serverlessworkflow/impl/{WorkflowState.java => WorkflowStatus.java} (88%) create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorHelper.java create mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java delete mode 100644 impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java delete mode 100644 impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java diff --git a/impl/core/pom.xml b/impl/core/pom.xml index 940f79fa..b0cace0b 100644 --- a/impl/core/pom.xml +++ b/impl/core/pom.xml @@ -50,5 +50,10 @@ assertj-core test + + ch.qos.logback + logback-classic + test + diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/LongFilter.java b/impl/core/src/main/java/io/serverlessworkflow/impl/LongFilter.java new file mode 100644 index 00000000..91b1b6c5 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/LongFilter.java @@ -0,0 +1,21 @@ +/* + * 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; + +import java.util.function.BiFunction; + +@FunctionalInterface +public interface LongFilter extends BiFunction, Long> {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java index 8015c687..dde5a315 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -41,19 +41,6 @@ public TaskContext(JsonNode input, WorkflowPosition position) { this(input, null, position, Instant.now(), input, input, input, null, new HashMap<>()); } - public TaskContext copy() { - return new TaskContext( - rawInput, - task, - position.copy(), - startedAt, - input, - output, - rawOutput, - flowDirective, - new HashMap<>(contextVariables)); - } - public TaskContext(JsonNode input, TaskContext taskContext, T task) { this( input, @@ -88,6 +75,19 @@ private TaskContext( this.contextVariables = contextVariables; } + public TaskContext copy() { + return new TaskContext( + rawInput, + task, + position.copy(), + startedAt, + input, + output, + rawOutput, + flowDirective, + new HashMap<>(contextVariables)); + } + public void input(JsonNode input) { this.input = input; this.rawOutput = input; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index 39d03809..df5b70e1 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -49,22 +49,19 @@ public class WorkflowDefinition implements AutoCloseable { private WorkflowDefinition( WorkflowApplication application, Workflow workflow, ResourceLoader resourceLoader) { - this.workflow = workflow; this.application = application; this.resourceLoader = resourceLoader; if (workflow.getInput() != null) { Input input = workflow.getInput(); this.inputSchemaValidator = - getSchemaValidator( - application.validatorFactory(), schemaToNode(resourceLoader, input.getSchema())); + getSchemaValidator(application.validatorFactory(), resourceLoader, input.getSchema()); this.inputFilter = buildWorkflowFilter(application.expressionFactory(), input.getFrom()); } if (workflow.getOutput() != null) { Output output = workflow.getOutput(); this.outputSchemaValidator = - getSchemaValidator( - application.validatorFactory(), schemaToNode(resourceLoader, output.getSchema())); + getSchemaValidator(application.validatorFactory(), resourceLoader, output.getSchema()); this.outputFilter = buildWorkflowFilter(application.expressionFactory(), output.getAs()); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index 8225439f..f81a6f24 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -19,11 +19,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.NullNode; +import io.serverlessworkflow.impl.executors.TaskExecutorHelper; import java.time.Instant; import java.util.concurrent.atomic.AtomicReference; public class WorkflowInstance { - private final AtomicReference state; + private final AtomicReference status; private final TaskContext taskContext; private final String id; private final JsonNode input; @@ -40,15 +41,16 @@ public class WorkflowInstance { definition .inputFilter() .ifPresent(f -> taskContext.input(f.apply(workflowContext, taskContext, input))); - state = new AtomicReference<>(WorkflowState.STARTED); + status = new AtomicReference<>(WorkflowStatus.RUNNING); context = new AtomicReference<>(NullNode.getInstance()); - WorkflowUtils.processTaskList(definition.workflow().getDo(), workflowContext, taskContext); + TaskExecutorHelper.processTaskList(definition.workflow().getDo(), workflowContext, taskContext); definition .outputFilter() .ifPresent( f -> taskContext.output(f.apply(workflowContext, taskContext, taskContext.rawOutput()))); definition.outputSchemaValidator().ifPresent(v -> v.validate(taskContext.output())); + status.compareAndSet(WorkflowStatus.RUNNING, WorkflowStatus.COMPLETED); } public String id() { @@ -67,12 +69,12 @@ public JsonNode context() { return context.get(); } - public WorkflowState state() { - return state.get(); + public WorkflowStatus status() { + return status.get(); } - public void state(WorkflowState state) { - this.state.set(state); + public void status(WorkflowStatus state) { + this.status.set(state); } public Object output() { diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowState.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowStatus.java similarity index 88% rename from impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowState.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowStatus.java index 310dbd0b..bc657839 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowState.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowStatus.java @@ -15,8 +15,11 @@ */ package io.serverlessworkflow.impl; -public enum WorkflowState { - STARTED, +public enum WorkflowStatus { + PENDING, + RUNNING, WAITING, - COMPLETED + COMPLETED, + FAULTED, + CANCELLED } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java index 56b3499a..0866ba05 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -19,14 +19,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.serverlessworkflow.api.WorkflowFormat; import io.serverlessworkflow.api.types.ExportAs; -import io.serverlessworkflow.api.types.FlowDirective; import io.serverlessworkflow.api.types.InputFrom; import io.serverlessworkflow.api.types.OutputAs; import io.serverlessworkflow.api.types.SchemaExternal; import io.serverlessworkflow.api.types.SchemaInline; import io.serverlessworkflow.api.types.SchemaUnion; -import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.impl.expressions.Expression; import io.serverlessworkflow.impl.expressions.ExpressionFactory; import io.serverlessworkflow.impl.expressions.ExpressionUtils; @@ -38,8 +35,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Optional; @@ -48,11 +43,12 @@ public class WorkflowUtils { private WorkflowUtils() {} public static Optional getSchemaValidator( - SchemaValidatorFactory validatorFactory, Optional node) { - return node.map(n -> validatorFactory.getValidator(n)); + SchemaValidatorFactory validatorFactory, ResourceLoader resourceLoader, SchemaUnion schema) { + return schemaToNode(resourceLoader, schema).map(n -> validatorFactory.getValidator(n)); } - public static Optional schemaToNode(ResourceLoader resourceLoader, SchemaUnion schema) { + private static Optional schemaToNode( + ResourceLoader resourceLoader, SchemaUnion schema) { if (schema != null) { if (schema.getSchemaInline() != null) { SchemaInline inline = schema.getSchemaInline(); @@ -94,18 +90,22 @@ public static Optional buildWorkflowFilter( public static StringFilter buildStringFilter( ExpressionFactory exprFactory, String expression, String literal) { - return expression != null ? from(buildWorkflowFilter(exprFactory, expression)) : from(literal); + return expression != null + ? toString(buildWorkflowFilter(exprFactory, expression)) + : toString(literal); } public static StringFilter buildStringFilter(ExpressionFactory exprFactory, String str) { - return ExpressionUtils.isExpr(str) ? from(buildWorkflowFilter(exprFactory, str)) : from(str); + return ExpressionUtils.isExpr(str) + ? toString(buildWorkflowFilter(exprFactory, str)) + : toString(str); } - public static StringFilter from(WorkflowFilter filter) { + private static StringFilter toString(WorkflowFilter filter) { return (w, t) -> filter.apply(w, t, t.input()).asText(); } - private static StringFilter from(String literal) { + private static StringFilter toString(String literal) { return (w, t) -> literal; } @@ -124,76 +124,19 @@ private static WorkflowFilter buildWorkflowFilter( throw new IllegalStateException("Both object and str are null"); } - private static TaskItem findTaskByName(ListIterator iter, String taskName) { - int currentIndex = iter.nextIndex(); - while (iter.hasPrevious()) { - TaskItem item = iter.previous(); - if (item.getName().equals(taskName)) { - return item; - } - } - while (iter.nextIndex() < currentIndex) { - iter.next(); - } - while (iter.hasNext()) { - TaskItem item = iter.next(); - if (item.getName().equals(taskName)) { - return item; - } - } - throw new IllegalArgumentException("Cannot find task with name " + taskName); + public static LongFilter buildLongFilter( + ExpressionFactory exprFactory, String expression, Long literal) { + return expression != null + ? toLong(buildWorkflowFilter(exprFactory, expression)) + : toLong(literal); } - public static void processTaskList( - List tasks, WorkflowContext context, TaskContext parentTask) { - parentTask.position().addProperty("do"); - TaskContext currentContext = parentTask; - if (!tasks.isEmpty()) { - ListIterator iter = tasks.listIterator(); - TaskItem nextTask = iter.next(); - while (nextTask != null) { - TaskItem task = nextTask; - parentTask.position().addIndex(iter.previousIndex()); - currentContext = executeTask(context, parentTask, task, currentContext.output()); - FlowDirective flowDirective = currentContext.flowDirective(); - if (flowDirective.getFlowDirectiveEnum() != null) { - switch (flowDirective.getFlowDirectiveEnum()) { - case CONTINUE: - nextTask = iter.hasNext() ? iter.next() : null; - break; - case END: - context.instance().state(WorkflowState.COMPLETED); - case EXIT: - nextTask = null; - break; - } - } else { - nextTask = WorkflowUtils.findTaskByName(iter, flowDirective.getString()); - } - parentTask.position().back(); - } - } - parentTask.position().back(); - parentTask.rawOutput(currentContext.output()); + private static LongFilter toLong(WorkflowFilter filter) { + return (w, t) -> filter.apply(w, t, t.input()).asLong(); } - public static TaskContext executeTask( - WorkflowContext context, TaskContext parentTask, TaskItem task, JsonNode input) { - parentTask.position().addProperty(task.getName()); - TaskContext result = - context - .definition() - .taskExecutors() - .computeIfAbsent( - parentTask.position().jsonPointer(), - k -> - context - .definition() - .taskFactory() - .getTaskExecutor(task.getTask(), context.definition())) - .apply(context, parentTask, input); - parentTask.position().back(); - return result; + private static LongFilter toLong(Long literal) { + return (w, t) -> literal; } public static WorkflowFilter buildWorkflowFilter(ExpressionFactory exprFactory, String str) { diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java index 44c6a7fd..f5ee1136 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -26,7 +26,6 @@ import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; import io.serverlessworkflow.impl.WorkflowFilter; -import io.serverlessworkflow.impl.WorkflowState; import io.serverlessworkflow.impl.jsonschema.SchemaValidator; import java.time.Instant; import java.util.Optional; @@ -55,8 +54,7 @@ private void buildInputProcessors(WorkflowDefinition definition) { this.inputProcessor = buildWorkflowFilter(definition.expressionFactory(), input.getFrom()); this.inputSchemaValidator = getSchemaValidator( - definition.validatorFactory(), - schemaToNode(definition.resourceLoader(), input.getSchema())); + definition.validatorFactory(), definition.resourceLoader(), input.getSchema()); } } @@ -66,8 +64,7 @@ private void buildOutputProcessors(WorkflowDefinition definition) { this.outputProcessor = buildWorkflowFilter(definition.expressionFactory(), output.getAs()); this.outputSchemaValidator = getSchemaValidator( - definition.validatorFactory(), - schemaToNode(definition.resourceLoader(), output.getSchema())); + definition.validatorFactory(), definition.resourceLoader(), output.getSchema()); } } @@ -79,8 +76,7 @@ private void buildContextProcessors(WorkflowDefinition definition) { } this.contextSchemaValidator = getSchemaValidator( - definition.validatorFactory(), - schemaToNode(definition.resourceLoader(), export.getSchema())); + definition.validatorFactory(), definition.resourceLoader(), export.getSchema()); } } @@ -88,31 +84,31 @@ private void buildContextProcessors(WorkflowDefinition definition) { public TaskContext apply( WorkflowContext workflowContext, TaskContext parentContext, JsonNode input) { TaskContext taskContext = new TaskContext<>(input, parentContext, task); - if (workflowContext.instance().state() == WorkflowState.COMPLETED) { - return taskContext; - } - workflowContext - .definition() - .listeners() - .forEach(l -> l.onTaskStarted(parentContext.position(), task)); + if (TaskExecutorHelper.isActive(workflowContext)) { + + workflowContext + .definition() + .listeners() + .forEach(l -> l.onTaskStarted(parentContext.position(), task)); - inputSchemaValidator.ifPresent(s -> s.validate(taskContext.rawInput())); - inputProcessor.ifPresent( - p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); - internalExecute(workflowContext, taskContext); - outputProcessor.ifPresent( - p -> taskContext.output(p.apply(workflowContext, taskContext, taskContext.rawOutput()))); - outputSchemaValidator.ifPresent(s -> s.validate(taskContext.output())); - contextProcessor.ifPresent( - p -> - workflowContext.context( - p.apply(workflowContext, taskContext, workflowContext.context()))); - contextSchemaValidator.ifPresent(s -> s.validate(workflowContext.context())); - taskContext.completedAt(Instant.now()); - workflowContext - .definition() - .listeners() - .forEach(l -> l.onTaskEnded(parentContext.position(), task)); + inputSchemaValidator.ifPresent(s -> s.validate(taskContext.rawInput())); + inputProcessor.ifPresent( + p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); + internalExecute(workflowContext, taskContext); + outputProcessor.ifPresent( + p -> taskContext.output(p.apply(workflowContext, taskContext, taskContext.rawOutput()))); + outputSchemaValidator.ifPresent(s -> s.validate(taskContext.output())); + contextProcessor.ifPresent( + p -> + workflowContext.context( + p.apply(workflowContext, taskContext, workflowContext.context()))); + contextSchemaValidator.ifPresent(s -> s.validate(workflowContext.context())); + taskContext.completedAt(Instant.now()); + workflowContext + .definition() + .listeners() + .forEach(l -> l.onTaskEnded(parentContext.position(), task)); + } return taskContext; } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index 89c099b6..e7dd07db 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -73,6 +73,8 @@ public TaskExecutor getTaskExecutor( return new TryExecutor(task.getTryTask(), definition); } else if (task.getForkTask() != null) { return new ForkExecutor(task.getForkTask(), definition); + } else if (task.getWaitTask() != null) { + return new WaitExecutor(task.getWaitTask(), definition); } throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java index 871a77da..c5dbc4fd 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java @@ -19,7 +19,6 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; -import io.serverlessworkflow.impl.WorkflowUtils; public class DoExecutor extends AbstractTaskExecutor { @@ -29,6 +28,6 @@ protected DoExecutor(DoTask task, WorkflowDefinition definition) { @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { - WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext); + TaskExecutorHelper.processTaskList(task.getDo(), workflow, taskContext); } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java index e74a18f9..cb4ecec0 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java @@ -52,7 +52,7 @@ protected void internalExecute(WorkflowContext workflow, TaskContext ta JsonNode item = iter.next(); taskContext.variables().put(task.getFor().getEach(), item); taskContext.variables().put(task.getFor().getAt(), i++); - WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext); + TaskExecutorHelper.processTaskList(task.getDo(), workflow, taskContext); } } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java index 484a6b9f..e0ce3b02 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java @@ -15,6 +15,7 @@ */ package io.serverlessworkflow.impl.executors; +import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.ForkTask; import io.serverlessworkflow.api.types.ForkTaskConfiguration; @@ -22,18 +23,17 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; -import io.serverlessworkflow.impl.WorkflowState; -import io.serverlessworkflow.impl.WorkflowUtils; -import io.serverlessworkflow.impl.generic.SortedArrayList; +import io.serverlessworkflow.impl.WorkflowStatus; import io.serverlessworkflow.impl.json.JsonUtils; import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.stream.Collectors; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,8 +47,6 @@ protected ForkExecutor(ForkTask task, WorkflowDefinition definition) { service = definition.executorService(); } - private record BranchContext(String taskName, TaskContext taskContext) {} - @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { ForkTaskConfiguration forkConfig = task.getFork(); @@ -62,13 +60,10 @@ protected void internalExecute(WorkflowContext workflow, TaskContext t item.getName(), service.submit(() -> executeBranch(workflow, taskContext.copy(), item, i))); } - List results = - new SortedArrayList<>( - (arg1, arg2) -> - arg1.taskContext.completedAt().compareTo(arg2.taskContext.completedAt())); + List>> results = new ArrayList<>(); for (Map.Entry>> entry : futures.entrySet()) { try { - results.add(new BranchContext(entry.getKey(), entry.getValue().get())); + results.add(Map.entry(entry.getKey(), entry.getValue().get())); } catch (ExecutionException ex) { Throwable cause = ex.getCause(); if (cause instanceof RuntimeException) { @@ -77,24 +72,25 @@ protected void internalExecute(WorkflowContext workflow, TaskContext t throw new UndeclaredThrowableException(ex); } } catch (InterruptedException ex) { - logger.warn( - "Thred executing branch {} was interrupted, this branch will be ignored", - entry.getKey(), - ex); + logger.warn("Branch {} was interrupted, no result will be recorded", entry.getKey(), ex); } } if (!results.isEmpty()) { + Stream>> sortedStream = + results.stream() + .sorted( + (arg1, arg2) -> + arg1.getValue().completedAt().compareTo(arg2.getValue().completedAt())); taskContext.rawOutput( forkConfig.isCompete() - ? results.get(0).taskContext().output() - : JsonUtils.fromValue( - results.stream() - .map( - e -> - JsonUtils.mapper() - .createObjectNode() - .set(e.taskName(), e.taskContext().output())) - .collect(Collectors.toList()))); + ? sortedStream.map(e -> e.getValue().output()).findFirst().orElseThrow() + : sortedStream + .map( + e -> + JsonUtils.mapper() + .createObjectNode() + .set(e.getKey(), e.getValue().output())) + .collect(JsonUtils.arrayNodeCollector())); } } } @@ -103,10 +99,10 @@ private TaskContext executeBranch( WorkflowContext workflow, TaskContext taskContext, TaskItem taskItem, int index) { taskContext.position().addIndex(index); TaskContext result = - WorkflowUtils.executeTask(workflow, taskContext, taskItem, taskContext.input()); + TaskExecutorHelper.executeTask(workflow, taskContext, taskItem, taskContext.input()); if (result.flowDirective() != null && result.flowDirective().getFlowDirectiveEnum() == FlowDirectiveEnum.END) { - workflow.instance().state(WorkflowState.COMPLETED); + workflow.instance().status(WorkflowStatus.COMPLETED); } taskContext.position().back(); return result; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorHelper.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorHelper.java new file mode 100644 index 00000000..e16cb085 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorHelper.java @@ -0,0 +1,111 @@ +/* + * 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.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.FlowDirective; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowStatus; +import java.util.List; +import java.util.ListIterator; + +public class TaskExecutorHelper { + private TaskExecutorHelper() {} + + public static void processTaskList( + List tasks, WorkflowContext context, TaskContext parentTask) { + parentTask.position().addProperty("do"); + TaskContext currentContext = parentTask; + if (!tasks.isEmpty()) { + ListIterator iter = tasks.listIterator(); + TaskItem nextTask = iter.next(); + while (nextTask != null && isActive(context)) { + TaskItem task = nextTask; + parentTask.position().addIndex(iter.previousIndex()); + currentContext = executeTask(context, parentTask, task, currentContext.output()); + FlowDirective flowDirective = currentContext.flowDirective(); + if (flowDirective.getFlowDirectiveEnum() != null) { + switch (flowDirective.getFlowDirectiveEnum()) { + case CONTINUE: + nextTask = iter.hasNext() ? iter.next() : null; + break; + case END: + context.instance().status(WorkflowStatus.COMPLETED); + break; + case EXIT: + nextTask = null; + break; + } + } else { + nextTask = findTaskByName(iter, flowDirective.getString()); + } + parentTask.position().back(); + } + } + parentTask.position().back(); + parentTask.rawOutput(currentContext.output()); + } + + public static boolean isActive(WorkflowContext context) { + return isActive(context.instance().status()); + } + + public static boolean isActive(WorkflowStatus status) { + return status == WorkflowStatus.RUNNING; + } + + public static TaskContext executeTask( + WorkflowContext context, TaskContext parentTask, TaskItem task, JsonNode input) { + parentTask.position().addProperty(task.getName()); + TaskContext result = + context + .definition() + .taskExecutors() + .computeIfAbsent( + parentTask.position().jsonPointer(), + k -> + context + .definition() + .taskFactory() + .getTaskExecutor(task.getTask(), context.definition())) + .apply(context, parentTask, input); + parentTask.position().back(); + return result; + } + + private static TaskItem findTaskByName(ListIterator iter, String taskName) { + int currentIndex = iter.nextIndex(); + while (iter.hasPrevious()) { + TaskItem item = iter.previous(); + if (item.getName().equals(taskName)) { + return item; + } + } + while (iter.nextIndex() < currentIndex) { + iter.next(); + } + while (iter.hasNext()) { + TaskItem item = iter.next(); + if (item.getName().equals(taskName)) { + return item; + } + } + throw new IllegalArgumentException("Cannot find task with name " + taskName); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java index 67af7995..eed2801b 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java @@ -48,7 +48,7 @@ protected TryExecutor(TryTask task, WorkflowDefinition definition) { @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { try { - WorkflowUtils.processTaskList(task.getTry(), workflow, taskContext); + TaskExecutorHelper.processTaskList(task.getTry(), workflow, taskContext); } catch (WorkflowException exception) { if (errorFilter.map(f -> f.test(exception.getWorflowError())).orElse(true) && whenFilter @@ -58,7 +58,7 @@ protected void internalExecute(WorkflowContext workflow, TaskContext ta .map(w -> !w.apply(workflow, taskContext, taskContext.input()).asBoolean()) .orElse(true)) { if (task.getCatch().getDo() != null) { - WorkflowUtils.processTaskList(task.getCatch().getDo(), workflow, taskContext); + TaskExecutorHelper.processTaskList(task.getCatch().getDo(), workflow, taskContext); } } else { throw exception; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java new file mode 100644 index 00000000..a1fb31c5 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java @@ -0,0 +1,58 @@ +/* + * 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.executors; + +import io.serverlessworkflow.api.types.DurationInline; +import io.serverlessworkflow.api.types.WaitTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WaitExecutor extends AbstractTaskExecutor { + + private static Logger logger = LoggerFactory.getLogger(WaitExecutor.class); + private final Duration millisToWait; + + protected WaitExecutor(WaitTask task, WorkflowDefinition definition) { + super(task, definition); + this.millisToWait = + task.getWait().getDurationInline() != null + ? toLong(task.getWait().getDurationInline()) + : Duration.parse(task.getWait().getDurationExpression()); + } + + private Duration toLong(DurationInline durationInline) { + Duration duration = Duration.ofMillis(durationInline.getMilliseconds()); + duration.plus(Duration.ofSeconds(durationInline.getSeconds())); + duration.plus(Duration.ofMinutes(durationInline.getMinutes())); + duration.plus(Duration.ofHours(durationInline.getHours())); + duration.plus(Duration.ofDays(durationInline.getDays())); + return duration; + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + try { + Thread.sleep(millisToWait.toMillis()); + } catch (InterruptedException e) { + logger.warn("Waiting thread was interrupted", e); + Thread.currentThread().interrupt(); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java b/impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java deleted file mode 100644 index e0647d1f..00000000 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/generic/SortedArrayList.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.generic; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; - -public class SortedArrayList extends ArrayList { - - private static final long serialVersionUID = 1L; - private final Comparator comparator; - - public SortedArrayList() { - this(SortedArrayList::defaultCompare); - } - - @SuppressWarnings("unchecked") - private static int defaultCompare(V a, V b) { - return a instanceof Comparable ? ((Comparable) a).compareTo(b) : 0; - } - - public SortedArrayList(Comparator comparator) { - this.comparator = comparator; - } - - public SortedArrayList(Collection collection) { - this(collection, SortedArrayList::defaultCompare); - } - - public SortedArrayList(Collection collection, Comparator comparator) { - super(collection.size()); - this.comparator = comparator; - addAll(collection); - } - - @Override - public boolean add(T object) { - int i; - for (i = 0; i < size() && comparator.compare(object, get(i)) >= 0; i++) {} - super.add(i, object); - return true; - } - - public boolean addAll(Collection c) { - ensureCapacity(size() + c.size()); - c.forEach(this::add); - return !c.isEmpty(); - } - - public T set(int index, T element) { - throw new UnsupportedOperationException("Do not allow adding in a particular index"); - } -} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java index a13c8313..0726c2be 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java @@ -36,9 +36,16 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; public class JsonUtils { @@ -48,6 +55,38 @@ public static ObjectMapper mapper() { return mapper; } + public static Collector arrayNodeCollector() { + return new Collector() { + @Override + public BiConsumer accumulator() { + return (arrayNode, item) -> arrayNode.add(item); + } + + @Override + public Set characteristics() { + return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); + } + + @Override + public BinaryOperator combiner() { + return (r1, r2) -> { + r1.addAll(r2); + return r1; + }; + } + + @Override + public Function finisher() { + return arrayNode -> arrayNode; + } + + @Override + public Supplier supplier() { + return () -> mapper.createArrayNode(); + } + }; + } + /* * Implementation note: * Although we can use directly ObjectMapper.convertValue for implementing fromValue and toJavaValue methods, diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java index d3ab3190..8982908f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java @@ -21,33 +21,22 @@ import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.ValidationMessage; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; public class DefaultSchemaValidator implements SchemaValidator { - private final JsonNode jsonNode; - private final AtomicReference schemaObject = new AtomicReference<>(); + private final JsonSchema schemaObject; public DefaultSchemaValidator(JsonNode jsonNode) { - this.jsonNode = jsonNode; + this.schemaObject = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(jsonNode); } @Override public void validate(JsonNode node) { - Set report = getSchema().validate(node); + Set report = schemaObject.validate(node); if (!report.isEmpty()) { StringBuilder sb = new StringBuilder("There are JsonSchema validation errors:"); report.forEach(m -> sb.append(System.lineSeparator()).append(m.getMessage())); throw new IllegalArgumentException(sb.toString()); } } - - private JsonSchema getSchema() { - JsonSchema result = schemaObject.get(); - if (result == null) { - result = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(jsonNode); - schemaObject.set(result); - } - return result; - } } diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java deleted file mode 100644 index 1ad0ed98..00000000 --- a/impl/core/src/test/java/io/serverlessworkflow/impl/SortedListTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.serverlessworkflow.impl.generic.SortedArrayList; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.Test; - -public class SortedListTest { - - private record Person(String name, int age) {} - - @Test - void testConstructor() { - assertThat(new SortedArrayList<>(Arrays.asList(3, 2, 1))).isEqualTo(Arrays.asList(1, 2, 3)); - } - - @Test - void testAdd() { - List list = new SortedArrayList<>(); - list.add(1); - list.add(4); - list.add(3); - list.add(2); - assertThat(list).isEqualTo(Arrays.asList(1, 2, 3, 4)); - } - - @Test - void testAddPojo() { - List list = new SortedArrayList<>((a, b) -> b.age() - a.age()); - list.add(new Person("Mariam", 5)); - list.add(new Person("Belen", 12)); - list.add(new Person("Alejandro", 7)); - list.add(new Person("Vicente", 16)); - list.add(new Person("Daniel", 14)); - assertThat(list.stream().map(Person::name)) - .isEqualTo(Arrays.asList("Vicente", "Daniel", "Belen", "Alejandro", "Mariam")); - } - - @Test - void testAddAll() { - List list = new SortedArrayList<>(); - Instant now = Instant.now(); - list.addAll(Arrays.asList(now.plusMillis(1000), now)); - assertThat(list.get(0)).isEqualTo(now); - } -} diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index e324de2a..e2a1dbf2 100644 --- a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -33,10 +33,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class WorkflowDefinitionTest { private static WorkflowApplication appl; + private static Logger logger = LoggerFactory.getLogger(WorkflowDefinitionTest.class); private static Instant before; @BeforeAll @@ -138,6 +141,7 @@ private static Arguments args( private static void checkNotCompeteOuput(WorkflowInstance instance) { JsonNode out = instance.outputAsJsonNode(); + logger.debug("Output is {}", out); assertThat(out).isInstanceOf(ArrayNode.class); assertThat(out).hasSize(2); ArrayNode array = (ArrayNode) out; diff --git a/impl/core/src/test/resources/fork-no-compete.yaml b/impl/core/src/test/resources/fork-no-compete.yaml index 5e78acf2..ee882f13 100644 --- a/impl/core/src/test/resources/fork-no-compete.yaml +++ b/impl/core/src/test/resources/fork-no-compete.yaml @@ -9,10 +9,20 @@ do: compete: false branches: - callNurse: - set: - patientId: John - room: 1 + do: + - waitForNurse: + wait: + milliseconds: 500 + - nurseArrived: + set: + patientId: John + room: 1 - callDoctor: - set: - patientId: Smith - room: 2 \ No newline at end of file + do: + - waitForDoctor: + wait: + milliseconds: 499 + - doctorArrived: + set: + patientId: Smith + room: 2 \ No newline at end of file diff --git a/impl/core/src/test/resources/fork.yaml b/impl/core/src/test/resources/fork.yaml index dfde183b..e9f3e7a3 100644 --- a/impl/core/src/test/resources/fork.yaml +++ b/impl/core/src/test/resources/fork.yaml @@ -9,7 +9,7 @@ do: compete: true branches: - callNurse: - set: + set: patientId: John room: 1 - callDoctor: From 17a036122b437844b65fbc351e499c914da373b1 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:23:34 +0100 Subject: [PATCH 21/22] Release alpha5.1 (#491) Signed-off-by: Francisco Javier Tirado Sarti --- .github/project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/project.yml b/.github/project.yml index 274b48af..633f883e 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -1,3 +1,3 @@ release: - current-version: 7.0.0-alpha5 + current-version: 7.0.0-alpha5.1 next-version: 7.0.0-SNAPSHOT From aad89e84a20a09faaac089c4eb2cd260c8f2dfeb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 4 Dec 2024 16:24:37 +0000 Subject: [PATCH 22/22] [maven-release-plugin] prepare release 7.0.0-alpha5.1 --- api/pom.xml | 2 +- custom-generator/pom.xml | 2 +- impl/bom/pom.xml | 2 +- impl/core/pom.xml | 4 ++-- impl/http/pom.xml | 2 +- impl/pom.xml | 2 +- pom.xml | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index f6186156..466a2754 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 serverlessworkflow-api diff --git a/custom-generator/pom.xml b/custom-generator/pom.xml index 7aaedbda..8444bb2a 100644 --- a/custom-generator/pom.xml +++ b/custom-generator/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 custom-generator diff --git a/impl/bom/pom.xml b/impl/bom/pom.xml index 63ef0fe3..604a8300 100644 --- a/impl/bom/pom.xml +++ b/impl/bom/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-impl - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 serverlessworkflow-impl-bom pom diff --git a/impl/core/pom.xml b/impl/core/pom.xml index b0cace0b..9fac9df6 100644 --- a/impl/core/pom.xml +++ b/impl/core/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-impl - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 serverlessworkflow-impl-core @@ -14,7 +14,7 @@ io.serverlessworkflow serverlessworkflow-api - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 com.github.f4b6a3 diff --git a/impl/http/pom.xml b/impl/http/pom.xml index 1b19e5a4..b67f2cd5 100644 --- a/impl/http/pom.xml +++ b/impl/http/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-impl - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 serverlessworkflow-impl-http diff --git a/impl/pom.xml b/impl/pom.xml index b49f8ab0..6a380017 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 serverlessworkflow-impl pom diff --git a/pom.xml b/pom.xml index 471a1b45..501bf66d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-SNAPSHOT + 7.0.0-alpha5.1 pom Serverless Workflow :: Parent @@ -33,7 +33,7 @@ scm:git:git@github.com:serverlessworkflow/sdk-java.git scm:git:git@github.com:serverlessworkflow/sdk-java.git https://github.com/serverlessworkflow/sdk-java - HEAD + 7.0.0-alpha5.1