io.serverlessworkflow.validation.WorkflowValidatorImpl Maven / Gradle / Ivy
/*
* 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.validation;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.serverlessworkflow.api.Workflow;
import io.serverlessworkflow.api.actions.Action;
import io.serverlessworkflow.api.branches.Branch;
import io.serverlessworkflow.api.events.EventDefinition;
import io.serverlessworkflow.api.events.OnEvents;
import io.serverlessworkflow.api.functions.FunctionDefinition;
import io.serverlessworkflow.api.interfaces.WorkflowValidator;
import io.serverlessworkflow.api.states.*;
import io.serverlessworkflow.api.switchconditions.EventCondition;
import io.serverlessworkflow.api.validation.ValidationError;
import io.serverlessworkflow.api.validation.WorkflowSchemaLoader;
import org.everit.json.schema.Schema;
import org.everit.json.schema.ValidationException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class WorkflowValidatorImpl implements WorkflowValidator {
private static final Logger logger = LoggerFactory.getLogger(WorkflowValidatorImpl.class);
private boolean schemaValidationEnabled = true;
private List validationErrors = new ArrayList();
private Schema workflowSchema = WorkflowSchemaLoader.getWorkflowSchema();
private String source;
private Workflow workflow;
@Override
public WorkflowValidator setWorkflow(Workflow workflow) {
this.workflow = workflow;
return this;
}
@Override
public WorkflowValidator setSource(String source) {
this.source = source;
return this;
}
@Override
public List validate() {
validationErrors.clear();
if (workflow == null) {
try {
if (schemaValidationEnabled && source != null) {
try {
if (!source.trim().startsWith("{")) {
// convert yaml to json to validate
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
Object obj = yamlReader.readValue(source, Object.class);
ObjectMapper jsonWriter = new ObjectMapper();
workflowSchema.validate(new JSONObject(jsonWriter.writeValueAsString(obj)));
} else {
workflowSchema.validate(new JSONObject(source));
}
} catch (ValidationException e) {
// ignore the "functions" and "events" multi-def error
if((!e.getMessage().equals("#/functions: expected type: JSONObject, found: JSONArray") &&
!e.getMessage().equals("#/events: expected type: JSONObject, found: JSONArray"))) {
// main error
addValidationError(e.getMessage(),
ValidationError.SCHEMA_VALIDATION);
// suberrors
e.getCausingExceptions().stream()
.map(ValidationException::getMessage)
.forEach(m -> addValidationError(m,
ValidationError.SCHEMA_VALIDATION));
}
}
}
} catch (Exception e) {
logger.error("Schema validation exception: " + e.getMessage());
}
}
// if there are schema validation errors
// there is no point of doing the workflow validation
if (validationErrors.size() > 0) {
return validationErrors;
} else {
if (workflow == null) {
workflow = Workflow.fromSource(source);
}
List functions = workflow.getFunctions() != null ? workflow.getFunctions().getFunctionDefs() : null;
List events = workflow.getEvents() != null? workflow.getEvents().getEventDefs() : null;
if (workflow.getId() == null || workflow.getId().trim().isEmpty()) {
addValidationError("Workflow id should not be empty",
ValidationError.WORKFLOW_VALIDATION);
}
if (workflow.getName() == null || workflow.getName().trim().isEmpty()) {
addValidationError("Workflow name should not be empty",
ValidationError.WORKFLOW_VALIDATION);
}
if (workflow.getVersion() == null || workflow.getVersion().trim().isEmpty()) {
addValidationError("Workflow version should not be empty",
ValidationError.WORKFLOW_VALIDATION);
}
if (workflow.getStates() == null || workflow.getStates().isEmpty()) {
addValidationError("No states found",
ValidationError.WORKFLOW_VALIDATION);
}
Validation validation = new Validation();
if (workflow.getStates() != null && !workflow.getStates().isEmpty()) {
workflow.getStates().forEach(s -> {
if (s.getName() != null && s.getName().trim().isEmpty()) {
addValidationError("State name should not be empty",
ValidationError.WORKFLOW_VALIDATION);
} else {
validation.addState(s.getName());
}
if (s.getStart() != null) {
validation.addStartState();
}
if (s.getEnd() != null) {
validation.addEndState();
}
if (s instanceof OperationState) {
OperationState operationState = (OperationState) s;
if (operationState.getActions() == null || operationState.getActions().size() < 1) {
addValidationError("Operation State has no actions defined",
ValidationError.WORKFLOW_VALIDATION);
}
List actions = operationState.getActions();
for (Action action : actions) {
if (action.getFunctionRef() != null) {
if (action.getFunctionRef().getRefName().isEmpty()) {
addValidationError("Operation State action functionRef should not be null or empty",
ValidationError.WORKFLOW_VALIDATION);
}
if (!haveFunctionDefinition(action.getFunctionRef().getRefName(), functions)) {
addValidationError("Operation State action functionRef does not reference an existing workflow function definition",
ValidationError.WORKFLOW_VALIDATION);
}
}
if (action.getEventRef() != null) {
if (action.getEventRef().getTriggerEventRef().isEmpty()) {
addValidationError("Operation State action trigger eventRef does not reference an existing workflow event definition",
ValidationError.WORKFLOW_VALIDATION);
}
if (action.getEventRef().getResultEventRef().isEmpty()) {
addValidationError("Operation State action results eventRef does not reference an existing workflow event definition",
ValidationError.WORKFLOW_VALIDATION);
}
if (!haveEventsDefinition(action.getEventRef().getTriggerEventRef(), events)) {
addValidationError("Operation State action trigger event def does not reference an existing workflow event definition",
ValidationError.WORKFLOW_VALIDATION);
}
if (!haveEventsDefinition(action.getEventRef().getResultEventRef(), events)) {
addValidationError("Operation State action results event def does not reference an existing workflow event definition",
ValidationError.WORKFLOW_VALIDATION);
}
}
}
}
if (s instanceof EventState) {
EventState eventState = (EventState) s;
if (eventState.getOnEvents() == null || eventState.getOnEvents().size() < 1) {
addValidationError("Event State has no eventActions defined",
ValidationError.WORKFLOW_VALIDATION);
}
List eventsActionsList = eventState.getOnEvents();
for (OnEvents onEvents : eventsActionsList) {
if (onEvents.getActions() == null || onEvents.getActions().size() < 1) {
addValidationError("Event State eventsActions has no actions",
ValidationError.WORKFLOW_VALIDATION);
}
List eventRefs = onEvents.getEventRefs();
if (eventRefs == null || eventRefs.size() < 1) {
addValidationError("Event State eventsActions has no event refs",
ValidationError.WORKFLOW_VALIDATION);
} else {
for (String eventRef : eventRefs) {
if (!haveEventsDefinition(eventRef, events)) {
addValidationError("Event State eventsActions eventRef does not match a declared workflow event definition",
ValidationError.WORKFLOW_VALIDATION);
}
}
}
}
}
if (s instanceof SwitchState) {
SwitchState switchState = (SwitchState) s;
if ((switchState.getDataConditions() == null || switchState.getDataConditions().size() < 1)
&& (switchState.getEventConditions() == null || switchState.getEventConditions().size() < 1)) {
addValidationError("Switch state should define either data or event conditions",
ValidationError.WORKFLOW_VALIDATION);
}
if (switchState.getDefault() == null) {
addValidationError("Switch state should define a default transition",
ValidationError.WORKFLOW_VALIDATION);
}
if (switchState.getEventConditions() != null && switchState.getEventConditions().size() > 0) {
List eventConditions = switchState.getEventConditions();
for (EventCondition ec : eventConditions) {
if (!haveEventsDefinition(ec.getEventRef(), events)) {
addValidationError("Switch state event condition eventRef does not reference a defined workflow event",
ValidationError.WORKFLOW_VALIDATION);
}
}
}
}
if (s instanceof DelayState) {
DelayState delayState = (DelayState) s;
if (delayState.getTimeDelay() == null || delayState.getTimeDelay().length() < 1) {
addValidationError("Delay state should have a non-empty time delay",
ValidationError.WORKFLOW_VALIDATION);
}
}
if (s instanceof ParallelState) {
ParallelState parallelState = (ParallelState) s;
if (parallelState.getBranches() == null || parallelState.getBranches().size() < 1) {
addValidationError("Parallel state should have branches",
ValidationError.WORKFLOW_VALIDATION);
}
List branches = parallelState.getBranches();
for (Branch branch : branches) {
if ((branch.getActions() == null || branch.getActions().size() < 1)
&& (branch.getWorkflowId() == null || branch.getWorkflowId().length() < 1)) {
addValidationError("Parallel state should define either actions or workflow id",
ValidationError.WORKFLOW_VALIDATION);
}
}
}
if (s instanceof SubflowState) {
SubflowState subflowState = (SubflowState) s;
if (subflowState.getWorkflowId() == null || subflowState.getWorkflowId().isEmpty()) {
addValidationError("SubflowState should have a valid workflow id",
ValidationError.WORKFLOW_VALIDATION);
}
}
if (s instanceof InjectState) {
InjectState injectState = (InjectState) s;
if (injectState.getData() == null) {
addValidationError("InjectState should have non-null data",
ValidationError.WORKFLOW_VALIDATION);
}
}
if (s instanceof ForEachState) {
ForEachState forEachState = (ForEachState) s;
if (forEachState.getInputCollection() == null || forEachState.getInputCollection().isEmpty()) {
addValidationError("ForEach state should have a valid inputCollection",
ValidationError.WORKFLOW_VALIDATION);
}
if (forEachState.getIterationParam() == null || forEachState.getIterationParam().isEmpty()) {
addValidationError("ForEach state should have a valid iteration parameter",
ValidationError.WORKFLOW_VALIDATION);
}
if ((forEachState.getActions() == null || forEachState.getActions().size() < 1)
&& (forEachState.getWorkflowId() == null || forEachState.getWorkflowId().length() < 1)) {
addValidationError("ForEach state should define either actions or workflow id",
ValidationError.WORKFLOW_VALIDATION);
}
}
if (s instanceof CallbackState) {
CallbackState callbackState = (CallbackState) s;
if (!haveEventsDefinition(callbackState.getEventRef(), events)) {
addValidationError("CallbackState event ref does not reference a defined workflow event definition",
ValidationError.WORKFLOW_VALIDATION);
}
if (haveFunctionDefinition(callbackState.getAction().getFunctionRef().getRefName(), functions)) {
addValidationError("CallbackState action function ref does not reference a defined workflow function definition",
ValidationError.WORKFLOW_VALIDATION);
}
}
});
if (validation.startStates == 0) {
addValidationError("No start state found.",
ValidationError.WORKFLOW_VALIDATION);
}
if (validation.startStates > 1) {
addValidationError("Multiple start states found.",
ValidationError.WORKFLOW_VALIDATION);
}
if (validation.endStates == 0) {
addValidationError("No end state found.",
ValidationError.WORKFLOW_VALIDATION);
}
}
return validationErrors;
}
}
@Override
public boolean isValid() {
return validate().size() < 1;
}
@Override
public WorkflowValidator setSchemaValidationEnabled(boolean schemaValidationEnabled) {
this.schemaValidationEnabled = schemaValidationEnabled;
return this;
}
@Override
public WorkflowValidator reset() {
workflow = null;
validationErrors.clear();
schemaValidationEnabled = true;
return this;
}
private boolean haveFunctionDefinition(String functionName, List functions) {
if(functions != null) {
FunctionDefinition fun = functions.stream().filter(f -> f.getName().equals(functionName))
.findFirst()
.orElse(null);
return fun == null ? false : true;
} else {
return false;
}
}
private boolean haveEventsDefinition(String eventName, List events) {
if(events != null) {
EventDefinition eve = events.stream().filter(e -> e.getName().equals(eventName))
.findFirst()
.orElse(null);
return eve == null ? false : true;
} else {
return false;
}
}
private void addValidationError(String message,
String type) {
ValidationError mainError = new ValidationError();
mainError.setMessage(message);
mainError.setType(type);
validationErrors.add(mainError);
}
private class Validation {
final Set events = new HashSet<>();
final Set functions = new HashSet();
final Set states = new HashSet<>();
Integer startStates = 0;
Integer endStates = 0;
void addFunction(String name) {
if (functions.contains(name)) {
addValidationError("Function does not have an unique name: " + name,
ValidationError.WORKFLOW_VALIDATION);
} else {
functions.add(name);
}
}
void addEvent(String name) {
if (events.contains(name)) {
addValidationError("Event does not have an unique name: " + name,
ValidationError.WORKFLOW_VALIDATION);
} else {
events.add(name);
}
}
void addState(String name) {
if (states.contains(name)) {
addValidationError("State does not have an unique name: " + name,
ValidationError.WORKFLOW_VALIDATION);
} else {
states.add(name);
}
}
void addStartState() {
startStates++;
}
void addEndState() {
endStates++;
}
}
}