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] [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