Repository: serverlessworkflow/sdk-java (java)

Issue:
Improve error message when no valid task is found
Currently for workflows that are not compliant with the spec, a long stack trace is printed as natural result of a failed parsing. 
This long stack trace contains details of the failure, but they are expressed as java exception, which are sometimes difficulty to interpret. 
It would be better (sacrificing some perfomance to increase usabillity) to validate the file againts the DSL schema before proceeding with the parsing. The validation will give the user, in one shot, all the format errors. 





Pull request description:
[Fix #498] Adding possibility to validate before parsing
Fix https://github.com/serverlessworkflow/sdk-java/issues/498


Patch:
diff --git a/api/pom.xml b/api/pom.xml
index f618615..eff5c34 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -21,6 +21,10 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-core</artifactId>
         </dependency>
+        <dependency>
+          <groupId>com.networknt</groupId>
+          <artifactId>json-schema-validator</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
diff --git a/api/src/main/java/io/serverlessworkflow/api/DirectReader.java b/api/src/main/java/io/serverlessworkflow/api/DirectReader.java
new file mode 100644
index 0000000..83fe055
--- /dev/null
+++ b/api/src/main/java/io/serverlessworkflow/api/DirectReader.java
@@ -0,0 +1,44 @@
+/*
+ * 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.api;
+
+import io.serverlessworkflow.api.types.Workflow;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+class DirectReader implements WorkflowReaderOperations {
+
+  @Override
+  public Workflow read(InputStream input, WorkflowFormat format) throws IOException {
+    return format.mapper().readValue(input, Workflow.class);
+  }
+
+  @Override
+  public Workflow read(Reader input, WorkflowFormat format) throws IOException {
+    return format.mapper().readValue(input, Workflow.class);
+  }
+
+  @Override
+  public Workflow read(byte[] input, WorkflowFormat format) throws IOException {
+    return format.mapper().readValue(input, Workflow.class);
+  }
+
+  @Override
+  public Workflow read(String input, WorkflowFormat format) throws IOException {
+    return format.mapper().readValue(input, Workflow.class);
+  }
+}
diff --git a/api/src/main/java/io/serverlessworkflow/api/ValidationReader.java b/api/src/main/java/io/serverlessworkflow/api/ValidationReader.java
new file mode 100644
index 0000000..25481d5
--- /dev/null
+++ b/api/src/main/java/io/serverlessworkflow/api/ValidationReader.java
@@ -0,0 +1,79 @@
+/*
+ * 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.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.networknt.schema.InputFormat;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SchemaValidatorsConfig;
+import com.networknt.schema.SpecVersion.VersionFlag;
+import com.networknt.schema.ValidationMessage;
+import io.serverlessworkflow.api.types.Workflow;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+class ValidationReader implements WorkflowReaderOperations {
+  private final JsonSchema schemaObject;
+
+  ValidationReader() {
+    try (InputStream input =
+        Thread.currentThread()
+            .getContextClassLoader()
+            .getResourceAsStream("schema/workflow.yaml")) {
+      this.schemaObject =
+          JsonSchemaFactory.getInstance(VersionFlag.V7)
+              .getSchema(input, InputFormat.YAML, SchemaValidatorsConfig.builder().build());
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
+    }
+  }
+
+  @Override
+  public Workflow read(InputStream input, WorkflowFormat format) throws IOException {
+    return validate(format.mapper().readValue(input, JsonNode.class), format);
+  }
+
+  @Override
+  public Workflow read(Reader input, WorkflowFormat format) throws IOException {
+    return validate(format.mapper().readValue(input, JsonNode.class), format);
+  }
+
+  @Override
+  public Workflow read(byte[] input, WorkflowFormat format) throws IOException {
+    return validate(format.mapper().readValue(input, JsonNode.class), format);
+  }
+
+  @Override
+  public Workflow read(String input, WorkflowFormat format) throws IOException {
+    return validate(format.mapper().readValue(input, JsonNode.class), format);
+  }
+
+  private Workflow validate(JsonNode value, WorkflowFormat format) {
+    Set<ValidationMessage> validationErrors = schemaObject.validate(value);
+    if (!validationErrors.isEmpty()) {
+      throw new IllegalArgumentException(
+          validationErrors.stream()
+              .map(ValidationMessage::toString)
+              .collect(Collectors.joining("\n")));
+    }
+    return format.mapper().convertValue(value, Workflow.class);
+  }
+}
diff --git a/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java b/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java
index 4decc69..f5eb3f2 100644
--- a/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java
+++ b/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java
@@ -16,58 +16,98 @@
 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;
 
 public class WorkflowReader {
 
   public static Workflow readWorkflow(InputStream input, WorkflowFormat format) throws IOException {
-    return format.mapper().readValue(input, Workflow.class);
+    return defaultReader().read(input, format);
   }
 
   public static Workflow readWorkflow(Reader input, WorkflowFormat format) throws IOException {
-    return format.mapper().readValue(input, Workflow.class);
+    return defaultReader().read(input, format);
   }
 
-  public static Workflow readWorkflow(Path path, WorkflowFormat format) throws IOException {
-    return format.mapper().readValue(Files.readAllBytes(path), Workflow.class);
+  public static Workflow readWorkflow(byte[] input, WorkflowFormat format) throws IOException {
+    return defaultReader().read(input, format);
   }
 
-  public static Workflow readWorkflow(byte[] content, WorkflowFormat format) throws IOException {
-    try (InputStream input = new ByteArrayInputStream(content)) {
-      return readWorkflow(input, format);
-    }
+  public static Workflow readWorkflow(Path path) throws IOException {
+    return readWorkflow(defaultReader(), path, WorkflowFormat.fromPath(path));
+  }
+
+  public static Workflow readWorkflow(Path path, WorkflowFormat format) throws IOException {
+    return readWorkflow(defaultReader(), path, format);
   }
 
-  public static Workflow readWorkflowFromString(String content, WorkflowFormat format)
+  public static Workflow readWorkflowFromString(String input, WorkflowFormat format)
       throws IOException {
-    try (Reader reader = new StringReader(content)) {
-      return readWorkflow(reader, format);
-    }
+    return defaultReader().read(input, format);
   }
 
   public static Workflow readWorkflowFromClasspath(String classpath) throws IOException {
+    return readWorkflowFromClasspath(defaultReader(), classpath);
+  }
+
+  public static Workflow readWorkflowFromClasspath(
+      String classpath, ClassLoader cl, WorkflowFormat format) throws IOException {
+    return readWorkflowFromClasspath(defaultReader(), classpath);
+  }
+
+  public static Workflow readWorkflow(WorkflowReaderOperations reader, Path path)
+      throws IOException {
+    return readWorkflow(reader, path, WorkflowFormat.fromPath(path));
+  }
+
+  public static Workflow readWorkflow(
+      WorkflowReaderOperations reader, Path path, WorkflowFormat format) throws IOException {
+    return reader.read(Files.readAllBytes(path), format);
+  }
+
+  public static Workflow readWorkflowFromClasspath(
+      WorkflowReaderOperations reader, String classpath) throws IOException {
     return readWorkflowFromClasspath(
+        reader,
         classpath,
         Thread.currentThread().getContextClassLoader(),
         WorkflowFormat.fromFileName(classpath));
   }
 
   public static Workflow readWorkflowFromClasspath(
-      String classpath, ClassLoader cl, WorkflowFormat format) throws IOException {
+      WorkflowReaderOperations reader, String classpath, ClassLoader cl, WorkflowFormat format)
+      throws IOException {
     try (InputStream in = cl.getResourceAsStream(classpath)) {
       if (in == null) {
         throw new FileNotFoundException(classpath);
       }
-      return readWorkflow(in, format);
+      return reader.read(in, format);
     }
   }
 
+  public static WorkflowReaderOperations noValidation() {
+    return NoValidationHolder.instance;
+  }
+
+  public static WorkflowReaderOperations validation() {
+    return ValidationHolder.instance;
+  }
+
+  private static class NoValidationHolder {
+    private static final WorkflowReaderOperations instance = new DirectReader();
+  }
+
+  private static class ValidationHolder {
+    private static final WorkflowReaderOperations instance = new ValidationReader();
+  }
+
+  private static WorkflowReaderOperations defaultReader() {
+    return NoValidationHolder.instance;
+  }
+
   private WorkflowReader() {}
 }
diff --git a/api/src/main/java/io/serverlessworkflow/api/WorkflowReaderOperations.java b/api/src/main/java/io/serverlessworkflow/api/WorkflowReaderOperations.java
new file mode 100644
index 0000000..7049aba
--- /dev/null
+++ b/api/src/main/java/io/serverlessworkflow/api/WorkflowReaderOperations.java
@@ -0,0 +1,31 @@
+/*
+ * 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.api;
+
+import io.serverlessworkflow.api.types.Workflow;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+public interface WorkflowReaderOperations {
+  Workflow read(InputStream input, WorkflowFormat format) throws IOException;
+
+  Workflow read(Reader input, WorkflowFormat format) throws IOException;
+
+  Workflow read(byte[] input, WorkflowFormat format) throws IOException;
+
+  Workflow read(String input, WorkflowFormat format) throws IOException;
+}
diff --git a/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java b/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java
index 2911539..5980dee 100644
--- a/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java
+++ b/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java
@@ -19,7 +19,6 @@ 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;
@@ -49,10 +48,7 @@ public class WorkflowWriter {
 
   public static String workflowAsString(Workflow workflow, WorkflowFormat format)
       throws IOException {
-    try (Writer writer = new StringWriter()) {
-      writeWorkflow(writer, workflow, format);
-      return writer.toString();
-    }
+    return format.mapper().writeValueAsString(workflow);
   }
 
   public static byte[] workflowAsBytes(Workflow workflow, WorkflowFormat format)
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 df5b70e..5596f87 100644
--- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java
+++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java
@@ -124,7 +124,6 @@ public class WorkflowDefinition implements AutoCloseable {
   }
 
   public ResourceLoader resourceLoader() {
-
     return resourceLoader;
   }
 


Test patch:
diff --git a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java
index 81d10ec..5d58e8b 100644
--- a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java
+++ b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java
@@ -17,6 +17,7 @@ package io.serverlessworkflow.api;
 
 import static io.serverlessworkflow.api.WorkflowReader.readWorkflow;
 import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath;
+import static io.serverlessworkflow.api.WorkflowReader.validation;
 import static io.serverlessworkflow.api.WorkflowWriter.workflowAsBytes;
 import static io.serverlessworkflow.api.WorkflowWriter.workflowAsString;
 import static io.serverlessworkflow.api.WorkflowWriter.writeWorkflow;
@@ -53,13 +54,13 @@ public class FeaturesTest {
         "features/set.yaml",
         "features/switch.yaml",
         "features/try.yaml",
-        "features/listen.yaml",
+        "features/listen-to-any.yaml",
         "features/callFunction.yaml",
         "features/callCustomFunction.yaml",
         "features/call-http-query-parameters.yaml"
       })
   public void testSpecFeaturesParsing(String workflowLocation) throws IOException {
-    Workflow workflow = readWorkflowFromClasspath(workflowLocation);
+    Workflow workflow = readWorkflowFromClasspath(validation(), workflowLocation);
     assertWorkflow(workflow);
     assertWorkflowEquals(workflow, writeAndReadInMemory(workflow));
   }
diff --git a/api/src/test/resources/features/callCustomFunction.yaml b/api/src/test/resources/features/callCustomFunction.yaml
index 4161cf4..fbb636b 100644
--- a/api/src/test/resources/features/callCustomFunction.yaml
+++ b/api/src/test/resources/features/callCustomFunction.yaml
@@ -1,27 +1,25 @@
 document:
-  dsl: 1.0.0-alpha5
-  namespace: test
-  name: call-example
-  version: 0.1.0
-schedule:
-  cron: 0 8 * * *
+  dsl: '1.0.0-alpha5'
+  namespace: samples
+  name: call-custom-function-inline
+  version: '0.1.0'
+use:
+  functions:
+    getPetById:
+      input:
+        schema:
+          document:
+            type: object
+            properties:
+              petId:
+                type: string
+            required: [ petId ]
+      call: http
+      with:
+        method: get
+        endpoint: https://petstore.swagger.io/v2/pet/{petId}
 do:
-- getData:
-    call: http
-    with:
-      method: get
-      endpoint: https://api.agify.io?name=meelad
-  output:
-    as: ".data.reading"
-- filterData:
-    for:
-      in: ".data.reading"
-      each: reading
-    do:
-    - log:
-        call: https://raw.githubusercontent.com/serverlessworkflow/catalog/main/functions/log/1.0.0/function.yaml
-        with:
-          level: information
-          format: "{TIMESTAMP} [{LEVEL}] ({CONTEXT}): {MESSAGE}"
-          message: Hello, world!
-          timestamp: true
\ No newline at end of file
+  - getPet:
+      call: getPetById
+      with:
+        petId: 69
\ No newline at end of file
diff --git a/api/src/test/resources/features/callOpenAPI.yaml b/api/src/test/resources/features/callOpenAPI.yaml
index 1a1d0c5..82843c5 100644
--- a/api/src/test/resources/features/callOpenAPI.yaml
+++ b/api/src/test/resources/features/callOpenAPI.yaml
@@ -8,7 +8,7 @@ do:
       call: openapi
       with:
         document:
-          uri: "https://petstore.swagger.io/v2/swagger.json"
+          endpoint: "https://petstore.swagger.io/v2/swagger.json"
         operationId: findPetsByStatus
         parameters:
           status: ${ .status }
diff --git a/api/src/test/resources/features/listen-to-any.yaml b/api/src/test/resources/features/listen-to-any.yaml
new file mode 100644
index 0000000..fa8794d
--- /dev/null
+++ b/api/src/test/resources/features/listen-to-any.yaml
@@ -0,0 +1,16 @@
+document:
+  dsl: '1.0.0-alpha5'
+  namespace: test
+  name: listen-to-any
+  version: '0.1.0'
+do:
+  - callDoctor:
+      listen:
+        to:
+          any:
+          - with:
+              type: com.fake-hospital.vitals.measurements.temperature
+              data: ${ .temperature > 38 }
+          - with:
+              type: com.fake-hospital.vitals.measurements.bpm
+              data: ${ .bpm < 60 or .bpm > 100 }
\ No newline at end of file
diff --git a/api/src/test/resources/features/listen.yaml b/api/src/test/resources/features/listen.yaml
deleted file mode 100644
index 1c56c22..0000000
--- a/api/src/test/resources/features/listen.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-document:
-  dsl: 1.0.0-alpha1
-  namespace: default
-  name: listen-task
-  version: 1.0.0
-do:
-  - listenToSomething:
-      listen:
-        to:
-          any:
-            - with: 
-                source: pepe
-                type: pepe
\ No newline at end of file