Skip to content

Commit dd98c29

Browse files
committed
[Fix serverlessworkflow#467] Adding schema validation
1 parent 4e4f3a0 commit dd98c29

23 files changed

+717
-99
lines changed

impl/pom.xml

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<artifactId>serverlessworkflow-impl</artifactId>
99
<properties>
1010
<version.org.glassfish.jersey>3.1.9</version.org.glassfish.jersey>
11-
<version.net.thisptr>1.0.1</version.net.thisptr>
11+
<version.net.thisptr>1.1.0</version.net.thisptr>
1212
</properties>
1313
<dependencies>
1414
<dependency>
@@ -26,6 +26,10 @@
2626
<artifactId>jersey-media-json-jackson</artifactId>
2727
<version>${version.org.glassfish.jersey}</version>
2828
</dependency>
29+
<dependency>
30+
<groupId>com.networknt</groupId>
31+
<artifactId>json-schema-validator</artifactId>
32+
</dependency>
2933
<dependency>
3034
<groupId>net.thisptr</groupId>
3135
<artifactId>jackson-jq</artifactId>

impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java

+60-4
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,80 @@
1515
*/
1616
package io.serverlessworkflow.impl;
1717

18+
import static io.serverlessworkflow.impl.WorkflowUtils.*;
1819
import static io.serverlessworkflow.impl.json.JsonUtils.*;
1920

2021
import com.fasterxml.jackson.databind.JsonNode;
22+
import io.serverlessworkflow.api.types.Input;
23+
import io.serverlessworkflow.api.types.Output;
2124
import io.serverlessworkflow.api.types.TaskBase;
2225
import io.serverlessworkflow.api.types.TaskItem;
2326
import io.serverlessworkflow.api.types.Workflow;
2427
import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory;
2528
import io.serverlessworkflow.impl.executors.TaskExecutor;
2629
import io.serverlessworkflow.impl.executors.TaskExecutorFactory;
30+
import io.serverlessworkflow.impl.expressions.ExpressionFactory;
31+
import io.serverlessworkflow.impl.expressions.JQExpressionFactory;
2732
import io.serverlessworkflow.impl.json.JsonUtils;
33+
import io.serverlessworkflow.impl.jsonschema.DefaultSchemaValidatorFactory;
34+
import io.serverlessworkflow.impl.jsonschema.SchemaValidator;
35+
import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory;
36+
import io.serverlessworkflow.resources.DefaultResourceLoader;
37+
import io.serverlessworkflow.resources.ResourceLoader;
2838
import java.util.Collection;
2939
import java.util.Collections;
3040
import java.util.HashSet;
3141
import java.util.List;
3242
import java.util.Map;
43+
import java.util.Optional;
3344
import java.util.concurrent.ConcurrentHashMap;
3445

3546
public class WorkflowDefinition {
3647

3748
private WorkflowDefinition(
3849
Workflow workflow,
3950
TaskExecutorFactory taskFactory,
40-
Collection<WorkflowExecutionListener> listeners) {
51+
Collection<WorkflowExecutionListener> listeners,
52+
WorkflowFactories factories) {
4153
this.workflow = workflow;
4254
this.taskFactory = taskFactory;
4355
this.listeners = listeners;
56+
this.factories = factories;
57+
if (workflow.getInput() != null) {
58+
Input input = workflow.getInput();
59+
this.inputSchemaValidator =
60+
getSchemaValidator(
61+
factories.getValidatorFactory(), schemaToNode(factories, input.getSchema()));
62+
this.inputFilter = buildWorkflowFilter(factories.getExpressionFactory(), input.getFrom());
63+
}
64+
if (workflow.getOutput() != null) {
65+
Output output = workflow.getOutput();
66+
this.outputSchemaValidator =
67+
getSchemaValidator(
68+
factories.getValidatorFactory(), schemaToNode(factories, output.getSchema()));
69+
this.outputFilter = buildWorkflowFilter(factories.getExpressionFactory(), output.getAs());
70+
}
4471
}
4572

4673
private final Workflow workflow;
4774
private final Collection<WorkflowExecutionListener> listeners;
4875
private final TaskExecutorFactory taskFactory;
76+
private final WorkflowFactories factories;
77+
private Optional<SchemaValidator> inputSchemaValidator = Optional.empty();
78+
private Optional<SchemaValidator> outputSchemaValidator = Optional.empty();
79+
private Optional<WorkflowFilter> inputFilter = Optional.empty();
80+
private Optional<WorkflowFilter> outputFilter = Optional.empty();
81+
4982
private final Map<String, TaskExecutor<? extends TaskBase>> taskExecutors =
5083
new ConcurrentHashMap<>();
5184

5285
public static class Builder {
5386
private final Workflow workflow;
5487
private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get();
88+
private ExpressionFactory exprFactory = JQExpressionFactory.get();
5589
private Collection<WorkflowExecutionListener> listeners;
90+
private ResourceLoader resourceLoader = DefaultResourceLoader.get();
91+
private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get();
5692

5793
private Builder(Workflow workflow) {
5894
this.workflow = workflow;
@@ -71,13 +107,29 @@ public Builder withTaskExecutorFactory(TaskExecutorFactory factory) {
71107
return this;
72108
}
73109

110+
public Builder withExpressionFactory(ExpressionFactory factory) {
111+
this.exprFactory = factory;
112+
return this;
113+
}
114+
115+
public Builder withResourceLoader(ResourceLoader resourceLoader) {
116+
this.resourceLoader = resourceLoader;
117+
return this;
118+
}
119+
120+
public Builder withSchemaValidatorFactory(SchemaValidatorFactory factory) {
121+
this.schemaValidatorFactory = factory;
122+
return this;
123+
}
124+
74125
public WorkflowDefinition build() {
75126
return new WorkflowDefinition(
76127
workflow,
77128
taskFactory,
78129
listeners == null
79130
? Collections.emptySet()
80-
: Collections.unmodifiableCollection(listeners));
131+
: Collections.unmodifiableCollection(listeners),
132+
new WorkflowFactories(resourceLoader, exprFactory, schemaValidatorFactory));
81133
}
82134
}
83135

@@ -103,9 +155,13 @@ public class WorkflowInstance {
103155

104156
private WorkflowInstance(TaskExecutorFactory factory, JsonNode input) {
105157
this.output = input;
106-
this.state = State.STARTED;
158+
inputSchemaValidator.ifPresent(v -> v.validate(input));
107159
this.context = WorkflowContext.builder(input).build();
160+
inputFilter.ifPresent(f -> output = f.apply(context, Optional.empty(), output));
161+
this.state = State.STARTED;
108162
processDo(workflow.getDo());
163+
outputFilter.ifPresent(f -> output = f.apply(context, Optional.empty(), output));
164+
outputSchemaValidator.ifPresent(v -> v.validate(output));
109165
}
110166

111167
private void processDo(List<TaskItem> tasks) {
@@ -118,7 +174,7 @@ private void processDo(List<TaskItem> tasks) {
118174
taskExecutors
119175
.computeIfAbsent(
120176
context.position().jsonPointer(),
121-
k -> taskFactory.getTaskExecutor(task.getTask()))
177+
k -> taskFactory.getTaskExecutor(task.getTask(), factories))
122178
.apply(context, output);
123179
listeners.forEach(l -> l.onTaskEnded(context.position(), task.getTask()));
124180
context.position().back().back();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl;
17+
18+
import io.serverlessworkflow.impl.expressions.ExpressionFactory;
19+
import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory;
20+
import io.serverlessworkflow.resources.ResourceLoader;
21+
22+
public class WorkflowFactories {
23+
24+
private final ResourceLoader resourceLoader;
25+
private final ExpressionFactory expressionFactory;
26+
private final SchemaValidatorFactory validatorFactory;
27+
28+
public WorkflowFactories(
29+
ResourceLoader loaderFactory,
30+
ExpressionFactory expressionFactory,
31+
SchemaValidatorFactory validatorFactory) {
32+
super();
33+
this.resourceLoader = loaderFactory;
34+
this.expressionFactory = expressionFactory;
35+
this.validatorFactory = validatorFactory;
36+
}
37+
38+
public ResourceLoader getResourceLoader() {
39+
return resourceLoader;
40+
}
41+
42+
public ExpressionFactory getExpressionFactory() {
43+
return expressionFactory;
44+
}
45+
46+
public SchemaValidatorFactory getValidatorFactory() {
47+
return validatorFactory;
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl;
17+
18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import java.util.Optional;
20+
21+
@FunctionalInterface
22+
public interface WorkflowFilter {
23+
JsonNode apply(WorkflowContext workflow, Optional<TaskContext<?>> task, JsonNode node);
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl;
17+
18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import io.serverlessworkflow.api.types.ExportAs;
20+
import io.serverlessworkflow.api.types.ExternalResource;
21+
import io.serverlessworkflow.api.types.InputFrom;
22+
import io.serverlessworkflow.api.types.OutputAs;
23+
import io.serverlessworkflow.api.types.SchemaExternal;
24+
import io.serverlessworkflow.api.types.SchemaInline;
25+
import io.serverlessworkflow.api.types.SchemaUnion;
26+
import io.serverlessworkflow.impl.expressions.Expression;
27+
import io.serverlessworkflow.impl.expressions.ExpressionFactory;
28+
import io.serverlessworkflow.impl.expressions.ExpressionUtils;
29+
import io.serverlessworkflow.impl.json.JsonUtils;
30+
import io.serverlessworkflow.impl.jsonschema.SchemaValidator;
31+
import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory;
32+
import java.io.IOException;
33+
import java.io.InputStream;
34+
import java.io.UncheckedIOException;
35+
import java.util.Map;
36+
import java.util.Optional;
37+
38+
public class WorkflowUtils {
39+
40+
private WorkflowUtils() {}
41+
42+
public static byte[] loadExternalResource(ExternalResource externalResource) {
43+
// TODO implement this
44+
throw new UnsupportedOperationException(
45+
"loading of external resources has not been implemented yet");
46+
}
47+
48+
public static Optional<SchemaValidator> getSchemaValidator(
49+
SchemaValidatorFactory validatorFactory, Optional<JsonNode> node) {
50+
return node.map(n -> validatorFactory.getValidator(n));
51+
}
52+
53+
public static Optional<JsonNode> schemaToNode(WorkflowFactories factories, SchemaUnion schema) {
54+
if (schema != null) {
55+
if (schema.getSchemaInline() != null) {
56+
SchemaInline inline = schema.getSchemaInline();
57+
return Optional.of(JsonUtils.mapper().convertValue(inline.getDocument(), JsonNode.class));
58+
} else if (schema.getSchemaExternal() != null) {
59+
SchemaExternal external = schema.getSchemaExternal();
60+
try (InputStream in =
61+
factories.getResourceLoader().loadStatic(external.getResource()).open()) {
62+
return Optional.of(JsonUtils.mapper().readTree(in));
63+
} catch (IOException io) {
64+
throw new UncheckedIOException(io);
65+
}
66+
}
67+
}
68+
return Optional.empty();
69+
}
70+
71+
public static Optional<WorkflowFilter> buildWorkflowFilter(
72+
ExpressionFactory exprFactory, InputFrom from) {
73+
return from != null
74+
? Optional.of(buildWorkflowFilter(exprFactory, from.getString(), from.getObject()))
75+
: Optional.empty();
76+
}
77+
78+
public static Optional<WorkflowFilter> buildWorkflowFilter(
79+
ExpressionFactory exprFactory, OutputAs as) {
80+
return as != null
81+
? Optional.of(buildWorkflowFilter(exprFactory, as.getString(), as.getObject()))
82+
: Optional.empty();
83+
}
84+
85+
public static Optional<WorkflowFilter> buildWorkflowFilter(
86+
ExpressionFactory exprFactory, ExportAs as) {
87+
return as != null
88+
? Optional.of(buildWorkflowFilter(exprFactory, as.getString(), as.getObject()))
89+
: Optional.empty();
90+
}
91+
92+
private static WorkflowFilter buildWorkflowFilter(
93+
ExpressionFactory exprFactory, String str, Object object) {
94+
if (str != null) {
95+
Expression expression = exprFactory.getExpression(str);
96+
return expression::eval;
97+
} else {
98+
Object exprObj = ExpressionUtils.buildExpressionObject(object, exprFactory);
99+
return exprObj instanceof Map
100+
? (w, t, n) ->
101+
JsonUtils.fromValue(
102+
ExpressionUtils.evaluateExpressionMap((Map<String, Object>) exprObj, w, t, n))
103+
: (w, t, n) -> JsonUtils.fromValue(object);
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)