*/
private void validateForkJoin(NodeDef node, LiteWorkflowApp app, Deque<String> forkNodes, Deque<String> joinNodes,
Deque<String> path, boolean okTo) throws WorkflowException {
if (path.contains(node.getName())) {
// cycle
throw new WorkflowException(ErrorCode.E0741, node.getName(), Arrays.toString(path.toArray()));
}
path.push(node.getName());
// Make sure that we're not revisiting a node (that's not a Kill, Join, or End type) that's been visited before from an
// "ok to" transition; if its from an "error to" transition, then its okay to visit it multiple times. Also, because we
// traverse through join nodes multiple times, we have to make sure not to throw an exception here when we're really just
// re-walking the same execution path (this is why we need the visitedJoinNodes list used later)
if (okTo && !(node instanceof KillNodeDef) && !(node instanceof JoinNodeDef) && !(node instanceof EndNodeDef)) {
if (visitedOkNodes.contains(node.getName())) {
throw new WorkflowException(ErrorCode.E0743, node.getName());
}
visitedOkNodes.add(node.getName());
}
if (node instanceof StartNodeDef) {
String transition = node.getTransitions().get(0); // start always has only 1 transition
NodeDef tranNode = app.getNode(transition);
validateForkJoin(tranNode, app, forkNodes, joinNodes, path, okTo);
}
else if (node instanceof ActionNodeDef) {
String transition = node.getTransitions().get(0); // "ok to" transition
NodeDef tranNode = app.getNode(transition);
validateForkJoin(tranNode, app, forkNodes, joinNodes, path, okTo); // propogate okTo
transition = node.getTransitions().get(1); // "error to" transition
tranNode = app.getNode(transition);
validateForkJoin(tranNode, app, forkNodes, joinNodes, path, false); // use false
}
else if (node instanceof DecisionNodeDef) {
for(String transition : (new HashSet<String>(node.getTransitions()))) {
NodeDef tranNode = app.getNode(transition);
validateForkJoin(tranNode, app, forkNodes, joinNodes, path, okTo);
}
}
else if (node instanceof ForkNodeDef) {
forkNodes.push(node.getName());
for(String transition : (new HashSet<String>(node.getTransitions()))) {
NodeDef tranNode = app.getNode(transition);
validateForkJoin(tranNode, app, forkNodes, joinNodes, path, okTo);
}
forkNodes.pop();
if (!joinNodes.isEmpty()) {
joinNodes.pop();
}
}
else if (node instanceof JoinNodeDef) {
if (forkNodes.isEmpty()) {
// no fork for join to match with
throw new WorkflowException(ErrorCode.E0742, node.getName());
}
if (forkNodes.size() > joinNodes.size() && (joinNodes.isEmpty() || !joinNodes.peek().equals(node.getName()))) {
joinNodes.push(node.getName());
}
if (!joinNodes.peek().equals(node.getName())) {
// join doesn't match fork
throw new WorkflowException(ErrorCode.E0732, forkNodes.peek(), node.getName(), joinNodes.peek());
}
joinNodes.pop();
String currentForkNode = forkNodes.pop();
String transition = node.getTransitions().get(0); // join always has only 1 transition
NodeDef tranNode = app.getNode(transition);
// If we're already under a situation where okTo is false, use false (propogate it)
// Or if we've already visited this join node, use false (because we've already traversed this path before and we don't
// want to throw an exception from the check against visitedOkNodes)
if (!okTo || visitedJoinNodes.contains(node.getName())) {
validateForkJoin(tranNode, app, forkNodes, joinNodes, path, false);
// Else, use true because this is either the first time we've gone through this join node or okTo was already false
} else {
visitedJoinNodes.add(node.getName());
validateForkJoin(tranNode, app, forkNodes, joinNodes, path, true);
}
forkNodes.push(currentForkNode);
joinNodes.push(node.getName());
}
else if (node instanceof KillNodeDef) {
// do nothing
}
else if (node instanceof EndNodeDef) {
if (!forkNodes.isEmpty()) {
path.pop(); // = node
String parent = path.peek();
// can't go to an end node in a fork
throw new WorkflowException(ErrorCode.E0737, parent, node.getName());
}
}
else {
// invalid node type (shouldn't happen)
throw new WorkflowException(ErrorCode.E0740, node.getName());
}
path.pop();
}