/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.component.execution.api;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import de.rcenvironment.core.datamodel.api.EndpointCharacter;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.incubator.GraphvizUtils;
import de.rcenvironment.core.utils.incubator.GraphvizUtils.DotFileBuilder;
/**
* Encapsulates information about the workflow graph and processes particular graph-related requests.
*
* @author Doreen Seider
* @author Sascha Zur
*/
public class WorkflowGraph implements Serializable {
private static final long serialVersionUID = -2028814913500870207L;
// execution identifier of node -> WorkflowGraphNode
private final Map<String, WorkflowGraphNode> nodes;
// identifier created by WorkflowGraph#createEdgeIdentifier -> set of WorkflowGraphEdge
private final Map<String, Set<WorkflowGraphEdge>> edges;
private final Map<String, Map<String, Set<Deque<WorkflowGraphHop>>>> determinedHopsToDriverForReset = new HashMap<>();
private final Map<String, Map<String, Set<Deque<WorkflowGraphHop>>>> determinedHopsToDriverOnFailure = new HashMap<>();
private final Map<String, WorkflowGraphNode> determinedDriverNodes = new HashMap<>();
public WorkflowGraph(Map<String, WorkflowGraphNode> nodes, Map<String, Set<WorkflowGraphEdge>> edges) {
this.nodes = nodes;
this.edges = edges;
}
/**
* Returns {@link Deque}s of {@link WorkflowGraphHop}s that need to be traversed when node with given execution identifier need to reset
* its loop.
*
* @param startNodeExecutionId execution identifier of the node that need to reset its loop
* @return {@link Deque}s of {@link WorkflowGraphHop}s to traverse for each output of the node that need to reset its loop
*
* @throws ComponentExecutionException if searching for driver node fails (might be an user error)
*/
public Map<String, Set<Deque<WorkflowGraphHop>>> getHopsToTraverseWhenResetting(String startNodeExecutionId)
throws ComponentExecutionException {
return getHopsToTraverseToGetToLoopDriver(startNodeExecutionId, EndpointCharacter.SAME_LOOP,
determinedHopsToDriverForReset, true);
}
/**
* Returns {@link Deque}s of {@link WorkflowGraphHop}s that need to be traversed when node with given execution identifier failed within
* a fault-tolerant loop.
*
* @param startNodeEecutionId execution identifier of the node that failed within a fault-tolerant loop
* @return {@link Deque}s of {@link WorkflowGraphHop}s to traverse for each output of the node that failed
*
* @throws ComponentExecutionException if searching for driver node fails (might be an user error)
*/
public Map<String, Set<Deque<WorkflowGraphHop>>> getHopsToTraverseOnFailure(String startNodeEecutionId)
throws ComponentExecutionException {
Map<String, Set<Deque<WorkflowGraphHop>>> hopsDequesPerOutput;
if (nodes.get(startNodeEecutionId).isDriver()) {
hopsDequesPerOutput = getHopsToTraverseToGetToLoopDriver(startNodeEecutionId, EndpointCharacter.OUTER_LOOP,
determinedHopsToDriverOnFailure, false);
} else {
hopsDequesPerOutput = getHopsToTraverseToGetToLoopDriver(startNodeEecutionId, EndpointCharacter.SAME_LOOP,
determinedHopsToDriverOnFailure, false);
}
Set<String> visitedInputs = new HashSet<>();
Iterator<Entry<String, Set<Deque<WorkflowGraphHop>>>> hopsDequesPerOutputIterator = hopsDequesPerOutput.entrySet().iterator();
while (hopsDequesPerOutputIterator.hasNext()) {
Entry<String, Set<Deque<WorkflowGraphHop>>> hopsDequesForOutput = hopsDequesPerOutputIterator.next();
Iterator<Deque<WorkflowGraphHop>> hopsForOutputIterator = hopsDequesForOutput.getValue().iterator();
while (hopsForOutputIterator.hasNext()) {
Deque<WorkflowGraphHop> hops = hopsForOutputIterator.next();
String targetInputName = hops.getLast().getTargetInputName();
if (visitedInputs.contains(targetInputName)) {
hopsForOutputIterator.remove();
} else {
visitedInputs.add(targetInputName);
}
}
if (hopsDequesForOutput.getValue().isEmpty()) {
hopsDequesPerOutputIterator.remove();
}
}
return hopsDequesPerOutput;
}
/**
* Returns the {@link WorkflowGraphNode} of the driver that controls the node with the given execution identifier.
*
* @param executionIdentifier execution identifier of the node which driver needs to be found
* @return {@link WorkflowGraphNode} of the driver that controls the node with the given execution identifier.
*
* @throws ComponentExecutionException if searching for driver node fails (might be an user error)
*/
// TODO review the kind of exception: when is it thrown? is it caused by an user or only a developer error?
public WorkflowGraphNode getLoopDriver(String executionIdentifier) throws ComponentExecutionException {
if (!determinedDriverNodes.containsKey(executionIdentifier)) {
getHopsToTraverseOnFailure(executionIdentifier);
}
return determinedDriverNodes.get(executionIdentifier);
}
private Map<String, Set<Deque<WorkflowGraphHop>>> getHopsToTraverseToGetToLoopDriver(String startNodeExecutionId,
EndpointCharacter startEnpointCharacter, Map<String, Map<String, Set<Deque<WorkflowGraphHop>>>> alreadyDeterminedHops,
boolean isResetSearch) throws ComponentExecutionException {
synchronized (alreadyDeterminedHops) {
if (!alreadyDeterminedHops.containsKey(startNodeExecutionId)) {
Map<String, Set<Deque<WorkflowGraphHop>>> hopsDequesPerOutput = new HashMap<String, Set<Deque<WorkflowGraphHop>>>();
WorkflowGraphNode startNode = nodes.get(startNodeExecutionId);
for (String startNodeOutputId : startNode.getOutputIdentifiers()) {
Set<Deque<WorkflowGraphHop>> hopsDeques = new HashSet<Deque<WorkflowGraphHop>>();
if (edges.containsKey(WorkflowGraph.createEdgeKey(startNode, startNodeOutputId))) {
for (WorkflowGraphEdge startEdge : edges.get(WorkflowGraph.createEdgeKey(startNode, startNodeOutputId))) {
if (startNode.isDriver()) {
if (startEnpointCharacter.equals(startEdge.getOutputCharacter())) {
hopsDeques = startNewHopsSearch(startNode, startEdge, isResetSearch);
}
} else {
hopsDeques = startNewHopsSearch(startNode, startEdge, isResetSearch);
}
}
hopsDequesPerOutput.put(startNode.getEndpointName(startNodeOutputId), hopsDeques);
} else {
hopsDequesPerOutput.put(startNode.getEndpointName(startNodeOutputId), hopsDeques);
}
}
alreadyDeterminedHops.put(startNodeExecutionId, hopsDequesPerOutput);
}
}
return createSnapshotOfHopsDeques(alreadyDeterminedHops.get(startNodeExecutionId));
}
private Map<String, Set<Deque<WorkflowGraphHop>>> createSnapshotOfHopsDeques(Map<String, Set<Deque<WorkflowGraphHop>>> hopDeques) {
Map<String, Set<Deque<WorkflowGraphHop>>> hopsDequesSnapshot = new HashMap<>();
for (String outputName : hopDeques.keySet()) {
hopsDequesSnapshot.put(outputName, new HashSet<Deque<WorkflowGraphHop>>());
for (Deque<WorkflowGraphHop> hops : hopDeques.get(outputName)) {
hopsDequesSnapshot.get(outputName).add(new LinkedList<>(hops));
}
}
return hopsDequesSnapshot;
}
private Set<Deque<WorkflowGraphHop>> startNewHopsSearch(WorkflowGraphNode startNode, WorkflowGraphEdge edge, boolean isResetSearch)
throws ComponentExecutionException {
Set<Deque<WorkflowGraphHop>> hopsDeques = new HashSet<Deque<WorkflowGraphHop>>();
WorkflowGraphNode nextNode = nodes.get(edge.getTargetExecutionIdentifier());
Deque<WorkflowGraphHop> hopsDeque = new LinkedList<WorkflowGraphHop>();
WorkflowGraphHop firstHop = new WorkflowGraphHop(edge.getSourceExecutionIdentifier(),
startNode.getEndpointName(edge.getOutputIdentifier()), edge.getTargetExecutionIdentifier(),
nextNode.getEndpointName(edge.getInputIdentifier()), edge.getOutputIdentifier());
hopsDeque.add(firstHop);
determineHopsRecursively(startNode, edge, nextNode, hopsDeque, hopsDeques, isResetSearch);
return hopsDeques;
}
private void determineHopsRecursively(WorkflowGraphNode startNode, WorkflowGraphEdge edge, WorkflowGraphNode targetNode,
Deque<WorkflowGraphHop> hopsDeque, Set<Deque<WorkflowGraphHop>> hopsDeques, boolean isResetSearch)
throws ComponentExecutionException {
if (targetNode.getExecutionIdentifier().equals(startNode.getExecutionIdentifier())) {
// got to start node again
if (targetNode.isDriver() && isResetSearch) {
hopsDeques.add(hopsDeque);
}
return;
}
if (nodeAlreadyVisitedThatWay(hopsDeque, targetNode, edge)) {
return;
}
if (targetNode.isDriver()) {
if (EndpointCharacter.OUTER_LOOP.equals(edge.getInputCharacter())) {
// No end of the recursion
continueHopSearch(startNode, targetNode, hopsDeque, hopsDeques, isResetSearch, EndpointCharacter.OUTER_LOOP);
} else if (EndpointCharacter.SAME_LOOP.equals(edge.getInputCharacter())) {
hopsDeques.add(hopsDeque);
addNodeToDeterminedDriverNodes(startNode, targetNode);
}
} else {
// No end of the recursion
EndpointCharacter outputCharacterToConsider = EndpointCharacter.SAME_LOOP;
if (hasNodeOppositeOutputCharacters(targetNode)) {
switch (edge.getInputCharacter()) {
case SAME_LOOP:
outputCharacterToConsider = EndpointCharacter.OUTER_LOOP;
break;
case OUTER_LOOP:
outputCharacterToConsider = EndpointCharacter.SAME_LOOP;
break;
default:
throw new IllegalArgumentException("Unknown endpoint character: " + edge.getInputCharacter());
}
}
continueHopSearch(startNode, targetNode, hopsDeque, hopsDeques, isResetSearch, outputCharacterToConsider);
}
}
private boolean nodeAlreadyVisitedThatWay(Deque<WorkflowGraphHop> hopsDeque, WorkflowGraphNode nodeToVisit,
WorkflowGraphEdge usedEdge) {
Iterator<WorkflowGraphHop> hopsDequeIterator = hopsDeque.iterator();
while (hopsDequeIterator.hasNext()) {
WorkflowGraphHop hop = hopsDequeIterator.next();
if (hop.getHopExecutionIdentifier().equals(nodeToVisit.getExecutionIdentifier())) {
if (!hasNodeOppositeOutputCharacters(nodeToVisit)) {
return true;
} else {
// expect at least one edge otherwise it is no valid hop
EndpointCharacter hopEdgesOutputCharacter =
edges.get(WorkflowGraph.createEdgeKey(nodes.get(hop.getHopExecutionIdentifier()), hop.getHopOutputIdentifier()))
.iterator().next().getOutputCharacter();
EndpointCharacter usedEdgesInputCharacter = usedEdge.getInputCharacter();
// if node was already visited, it must be visited via a different input character than last time
// note: only outgoing edge can be get from the hop here, so the comparison of the newly visited input character is done
// with output character from last visit and must be equal then (to consider it as a new visit)
if (!hopEdgesOutputCharacter.equals(usedEdgesInputCharacter)) {
return true;
}
}
}
}
return false;
}
private boolean hasNodeOppositeOutputCharacters(WorkflowGraphNode node) {
boolean sameLoop = false;
boolean outerLoop = false;
for (String nodeOutputId : node.getOutputIdentifiers()) {
if (edges.containsKey(WorkflowGraph.createEdgeKey(node, nodeOutputId))) {
for (WorkflowGraphEdge edge : edges.get(WorkflowGraph.createEdgeKey(node, nodeOutputId))) {
if (edge.getOutputCharacter().equals(EndpointCharacter.SAME_LOOP)) {
sameLoop = true;
} else if (edge.getOutputCharacter().equals(EndpointCharacter.OUTER_LOOP)) {
outerLoop = true;
}
}
}
}
return sameLoop && outerLoop;
}
private void continueHopSearch(WorkflowGraphNode startNode, WorkflowGraphNode targetNode, Deque<WorkflowGraphHop> hopsDeque,
Set<Deque<WorkflowGraphHop>> hopsDeques, boolean isResetSearch, EndpointCharacter... outputCharacters)
throws ComponentExecutionException {
for (String targetNodeOutputId : targetNode.getOutputIdentifiers()) {
if (edges.containsKey(WorkflowGraph.createEdgeKey(targetNode, targetNodeOutputId))) {
for (WorkflowGraphEdge nextEdge : edges.get(WorkflowGraph.createEdgeKey(targetNode, targetNodeOutputId))) {
if (Arrays.asList(outputCharacters).contains(nextEdge.getOutputCharacter())) {
continueHopsSearch(startNode, targetNode, nextEdge, hopsDeque, hopsDeques, isResetSearch);
}
}
}
}
}
private void continueHopsSearch(WorkflowGraphNode startNode, WorkflowGraphNode currentNode, WorkflowGraphEdge edge,
Deque<WorkflowGraphHop> hopsDeque, Set<Deque<WorkflowGraphHop>> hopsDeques, boolean isResetSearch)
throws ComponentExecutionException {
Deque<WorkflowGraphHop> newHopDeque = new LinkedList<WorkflowGraphHop>(hopsDeque);
WorkflowGraphNode nextNode = nodes.get(edge.getTargetExecutionIdentifier());
WorkflowGraphHop nextHop = new WorkflowGraphHop(edge.getSourceExecutionIdentifier(),
currentNode.getEndpointName(edge.getOutputIdentifier()), edge.getTargetExecutionIdentifier(),
nextNode.getEndpointName(edge.getInputIdentifier()), edge.getOutputIdentifier());
newHopDeque.add(nextHop);
determineHopsRecursively(startNode, edge, nextNode, newHopDeque, hopsDeques, isResetSearch);
}
private void addNodeToDeterminedDriverNodes(WorkflowGraphNode startNode, WorkflowGraphNode driverNode)
throws ComponentExecutionException {
if (driverNode != null) {
if (determinedDriverNodes.get(startNode.getExecutionIdentifier()) == null) {
determinedDriverNodes.put(startNode.getExecutionIdentifier(), driverNode);
} else if (!determinedDriverNodes.get(startNode.getExecutionIdentifier()).getExecutionIdentifier()
.equals(driverNode.getExecutionIdentifier())) {
throw new ComponentExecutionException(
"Error in workflow graph search: newly determined driver node differs from driver node determined earlier");
}
}
}
/**
* Generates dot language string describing the workflow graph. Can be used to visualize the graph with Graphviz.
* (Command: dot -Tpng <wf.dot> -o <wf.png>)
*
* @return dot language string representation
*/
public String toDotScript() {
final String propNameColor = "color";
DotFileBuilder builder = GraphvizUtils.createDotFileBuilder("wf_graph");
// TODO properties should be set more efficiently (e.g., by defining shape, font size etc. as global properties of the digraph)
for (WorkflowGraphNode node : nodes.values()) {
builder.addVertex(node.getExecutionIdentifier(), node.getName());
if (node.isDriver()) {
builder.addVertexProperty(node.getExecutionIdentifier(), propNameColor, "#AA3939");
} else if (hasNodeOppositeOutputCharacters(node)) {
builder.addVertexProperty(node.getExecutionIdentifier(), propNameColor, "#D4AA6A");
}
builder.addVertexProperty(node.getExecutionIdentifier(), "shape", "rectangle");
builder.addVertexProperty(node.getExecutionIdentifier(), "fontsize", "10");
builder.addVertexProperty(node.getExecutionIdentifier(), "fontname", "Consolas");
}
for (Set<WorkflowGraphEdge> edgesSet : edges.values()) {
for (WorkflowGraphEdge edge : edgesSet) {
Map<String, String> edgeProps = new HashMap<>();
edgeProps.put("fontsize", "10");
edgeProps.put("fontname", "Consolas");
if (edge.getInputCharacter().equals(EndpointCharacter.OUTER_LOOP)) {
edgeProps.put(propNameColor, "#55AA55");
} else if (edge.getOutputCharacter().equals(EndpointCharacter.OUTER_LOOP)) {
edgeProps.put(propNameColor, "#4B698B");
}
String label = StringUtils.format("%s > %s", nodes.get(edge.getSourceExecutionIdentifier())
.getEndpointName(edge.getOutputIdentifier()), nodes.get(edge.getTargetExecutionIdentifier())
.getEndpointName(edge.getInputIdentifier()));
builder.addEdge(edge.getSourceExecutionIdentifier(), edge.getTargetExecutionIdentifier(), label, edgeProps);
}
}
return builder.getScriptContent();
}
/**
* Creates a key out of the {@link WorkflowGraphEdge}, which can be used as key of a map.
*
* @param edge {@link WorkflowGraphEdge} to get the identifier for
* @return key for the {@link WorkflowGraphEdge}
*/
public static String createEdgeKey(WorkflowGraphEdge edge) {
return StringUtils.escapeAndConcat(edge.getSourceExecutionIdentifier(), edge.getOutputIdentifier());
}
/**
* Creates a key out of the {@link WorkflowGraphNode}, which can be used as key of a map.
*
* @param node source {@link WorkflowGraphNode} of the edge to get the identifier for
* @param outputIdentifier source endpoint of the edge to get the identifier for
* @return key for the {@link WorkflowGraphEdge}
*/
public static String createEdgeKey(WorkflowGraphNode node, String outputIdentifier) {
return StringUtils.escapeAndConcat(node.getExecutionIdentifier(), outputIdentifier);
}
}