de.hpi.bpmn2_0.replay.ReplayTrace.java Source code

Java tutorial

Introduction

Here is the source code for de.hpi.bpmn2_0.replay.ReplayTrace.java

Source

/*
 * Copyright  2009-2016 The Apromore Initiative.
 *
 * This file is part of "Apromore".
 *
 * "Apromore" is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * "Apromore" is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/lgpl-3.0.html>.
 */

package de.hpi.bpmn2_0.replay;

import de.hpi.bpmn2_0.backtracking2.Node;
import de.hpi.bpmn2_0.backtracking2.StateElementStatus;
import de.hpi.bpmn2_0.model.FlowNode;
import de.hpi.bpmn2_0.model.connector.SequenceFlow;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Logger;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;

/*
* Note that this trace can be incomplete: not having end node
* depending on a particular log trace. For example, a fork node might not be
* always has a corresponding join node in the trace since halfway during replay
* the log trace runs out of events.
*/
public class ReplayTrace {
    private TraceNode startNode = null;
    private DateTime startDate = null;
    private DateTime endDate = null;
    private XTrace2 logTrace = null;
    private long algoRuntime = 0; //milliseconds to run backtracking algo for this trace

    //All nodes in the trace. Different from model nodes because there can be different trace nodes
    //corresponding to one model node (as a result of loop effect in the model)
    //private ArrayList<TraceNode> traceNodes = new ArrayList();

    //Contains replayed node in the order of timing. Only contain node of model that is actually played
    //Every element points to a trace node in the replayed trace
    //This is to support for the replayed trace, like its flatten form in timing order
    private ArrayList<TraceNode> timeOrderedReplayedNodes = new ArrayList();

    //All sequence flows in the trace.
    private ArrayList<SequenceFlow> sequenceFlows = new ArrayList();

    //This is a mapping from the marking in BPMN model to the marking of this trace
    //Note that the different between BPMN model and this trace is this trace has flatten structure
    //Meaning repeated path of events on BPMN model is replicated as new paths on this trace
    //key: one node in the current marking of BPMN model
    //value: corresponding node in current marking of this trace
    //During replay, markings of BPMN model and this trace must be kept synchronized.
    private Map<FlowNode, TraceNode> markingsMap = new HashMap();

    //private BidiMap<TraceNode,TraceNode> forkJoinMap = new DualHashBidiMap<>();
    //private BidiMap<TraceNode,TraceNode> ORforkJoinMap = new DualHashBidiMap<>();

    private Replayer replayer;

    private Node backtrackingNode;

    private static final Logger LOGGER = Logger.getLogger(ReplayTrace.class.getCanonicalName());

    //fitness metrics
    double mlCost = 0;
    double mmCost = 0;
    double mSyncCost = 0;
    double mlUpper = 0;
    double mmUpper = 0;
    double traceFitness = -1;
    boolean isFitnessMetricsCalculated = false;

    public ReplayTrace(XTrace2 trace, Replayer replayer, Node backtrackingNode, long algoRuntime) {
        this.logTrace = trace;
        this.replayer = replayer;
        this.backtrackingNode = backtrackingNode;
        this.algoRuntime = algoRuntime;
    }

    public boolean isReliable() {
        return ((backtrackingNode != null)
                && (backtrackingNode.getState().getTraceIndex() == backtrackingNode.getState().getTrace().size())
                && (backtrackingNode.getState().isProperCompletion()));
    }

    public String getId() {
        if (logTrace != null) {
            return logTrace.getId();
        } else {
            return null;
        }
    }

    public XTrace2 getOriginalTrace() {
        return this.logTrace;
    }

    public TraceNode getStart() {
        return startNode;
    }

    public void setStart(TraceNode startNode) {
        this.startNode = startNode;
        this.markingsMap.put(startNode.getModelNode(), startNode);
        /*
        if (!this.traceNodes.contains(startNode)) {
        this.traceNodes.add(startNode);
        }
        */
    }

    public DateTime getStartDate() {
        return timeOrderedReplayedNodes.get(0).getStart();
    }

    public DateTime getEndDate() {
        return timeOrderedReplayedNodes.get(timeOrderedReplayedNodes.size() - 1).getComplete();
    }

    public Interval getInterval() {
        if (this.getStartDate() != null && this.getEndDate() != null) {
            return new Interval(this.getStartDate(), this.getEndDate());
        } else {
            return null;
        }
    }

    public long getAlgoRuntime() {
        return this.algoRuntime;
    }

    //newNode: node to add
    //curModelNode: source node to be connected with the new node
    public void add(FlowNode curModelNode, TraceNode newNode) {
        FlowNode newModelNode = newNode.getModelNode();
        SequenceFlow modelFlow = null;

        TraceNode curNode = this.getMarkingsMap().get(curModelNode);

        SequenceFlow traceFlow = new SequenceFlow();
        traceFlow.setSourceRef(curNode);
        traceFlow.setTargetRef(newNode);

        //Search for original sequence id
        for (SequenceFlow flow : curModelNode.getOutgoingSequenceFlows()) {
            if (newModelNode == flow.getTargetRef()) {
                modelFlow = flow;
                break;
            }
        }
        if (modelFlow != null) {
            traceFlow.setId(modelFlow.getId());
        }

        curNode.getOutgoing().add(traceFlow);
        newNode.getIncoming().add(traceFlow);

        this.markingsMap.put(newNode.getModelNode(), newNode);
    }

    //nodeSet: set of branch nodes of a join
    //joinNode: join node to add
    public void add(Collection<FlowNode> nodeSet, TraceNode joinNode) {
        SequenceFlow traceFlow;
        SequenceFlow modelFlow = null;
        TraceNode branchNode;
        FlowNode newModelNode = joinNode.getModelNode();

        for (FlowNode node : nodeSet) {
            branchNode = this.getMarkingsMap().get(node);

            traceFlow = new SequenceFlow();
            traceFlow.setSourceRef(branchNode);
            traceFlow.setTargetRef(joinNode);

            for (SequenceFlow flow : node.getOutgoingSequenceFlows()) {
                if (newModelNode == flow.getTargetRef()) {
                    modelFlow = flow;
                    break;
                }
            }
            if (modelFlow != null) {
                traceFlow.setId(modelFlow.getId());
            }

            branchNode.getOutgoing().add(traceFlow);
            joinNode.getIncoming().add(traceFlow);
        }

        this.markingsMap.put(joinNode.getModelNode(), joinNode);
    }

    //Add the input node to the list of nodes which have been replayed
    //This method assumes that the replayed node has been added to the list of 
    //all nodes in the replayed trace.
    public void addToReplayedList(FlowNode curModelNode) {
        this.timeOrderedReplayedNodes.add(this.markingsMap.get(curModelNode));
    }

    //Return the replayed trace nodes in the replayed timing order
    public List<TraceNode> getTimeOrderedReplayedNodes() {
        return this.timeOrderedReplayedNodes;
    }

    public void setMatchedActivity(FlowNode curNode) {
        TraceNode traceNode = this.markingsMap.get(curNode);
        traceNode.setActivityMatched(true);
    }

    public void setVirtual(FlowNode curNode) {
        TraceNode traceNode = this.markingsMap.get(curNode);
        traceNode.setActivitySkipped(true);
    }

    public void setNodeTime(FlowNode node, Date date) {
        this.markingsMap.get(node).setStart(new DateTime(date));
    }

    public ArrayList<TraceNode> getNodes() {
        //return traceNodes;
        return this.timeOrderedReplayedNodes;
    }

    public ArrayList<SequenceFlow> getSequenceFlows() {
        if (this.sequenceFlows.isEmpty()) {
            for (TraceNode node : this.timeOrderedReplayedNodes) {
                for (SequenceFlow flow : node.getOutgoingSequenceFlows()) {
                    this.sequenceFlows.add(flow);
                }
            }
        }
        return this.sequenceFlows;
    }

    public Map<FlowNode, TraceNode> getMarkingsMap() {
        return markingsMap;
    }

    public Node getBacktrackingNode() {
        return this.backtrackingNode;
    }

    public void clear() {
        startNode = null;
        logTrace = null;

        //Remove dependency between nodes
        for (SequenceFlow flow : this.sequenceFlows) {
            flow.setSourceRef(null);
            flow.setTargetRef(null);
        }
        sequenceFlows.clear();
        timeOrderedReplayedNodes.clear();
        markingsMap.clear();
        replayer = null;
        backtrackingNode = null;
    }

    public boolean isEmpty() {
        return (startNode == null || this.timeOrderedReplayedNodes.size() == 0);
    }

    /*
     * MoveLogFitness = 1 - TotalMoveOnLogOnlyCost / AllMoveOnLogCost
     * TotalMoveOnLogOnlyCost: total cost of EVENT_SKIP node in replay trace
     * AllMoveOnLogCost: total cost of both ACTIVITY_MATCHED (meaning sync move on both) and EVENT_SKIP node in replay trace
     */
    public double getCostBasedMoveLogFitness() {
        double totalMoveOnLogOnlyCost = 0;
        double allMoveOnLogCost = 0;

        Node node = backtrackingNode;
        while (node != null) {
            if (node.getState().getElementStatus() == StateElementStatus.EVENT_SKIPPED) {
                totalMoveOnLogOnlyCost += 1;
                allMoveOnLogCost += 1;
            } else if (node.getState().getElementStatus() == StateElementStatus.ACTIVITY_MATCHED) {
                allMoveOnLogCost += 1;
            }
            node = node.getParent();
        }

        //The original trace might not be played fully, consider all remaining events as EVENT_SKIP
        if (backtrackingNode != null && !backtrackingNode.getState().isTraceFinished()) {
            totalMoveOnLogOnlyCost += (backtrackingNode.getState().getTrace().size()
                    - backtrackingNode.getState().getTraceIndex());
            allMoveOnLogCost += (backtrackingNode.getState().getTrace().size()
                    - backtrackingNode.getState().getTraceIndex());
        }

        if (allMoveOnLogCost > 0) {
            return 1 - (1.0 * totalMoveOnLogOnlyCost / allMoveOnLogCost);
        } else {
            return 1.00;
        }
    }

    /*
     * MoveModelFitness = 1 - TotalMoveOnModelOnlyCost / AllMoveOnModelCost
     * TotalMoveOnModelOnlyCost: total cost of ACTIVITY_SKIP node in the replay trace
     * AllMoveOnModelCost: total cost of both ACTIVITY_MATCHED (sync move) and ACTIVITY_SKIP node in the replay trace     
     */
    public double getCostBasedMoveModelFitness() {
        double totalMoveOnModelOnlyCost = 0;
        double allMoveOnModelCost = 0;

        Node node = backtrackingNode;
        while (node != null) {
            if (node.getState().getElementStatus() == StateElementStatus.ACTIVITY_SKIPPED) {
                totalMoveOnModelOnlyCost += 1;
                allMoveOnModelCost += 1;
            } else if (node.getState().getElementStatus() == StateElementStatus.ACTIVITY_MATCHED) {
                allMoveOnModelCost += 1;
            }
            node = node.getParent();
        }

        if (allMoveOnModelCost > 0) {
            return 1 - (1.0 * totalMoveOnModelOnlyCost / allMoveOnModelCost);
        } else {
            return 1.00;
        }
    }

    public void calcFitnessMetrics() {
        //Remember to reset these since this method can be called multiple times
        mlCost = 0;
        mmCost = 0;
        mSyncCost = 0;
        mmUpper = 0;
        mlUpper = this.logTrace.size() * replayer.getReplayParams().getEventSkipCost();

        Node node = backtrackingNode;
        while (node != null) {
            if (node.getState().getElementStatus() == StateElementStatus.EVENT_SKIPPED) {
                mlCost += replayer.getReplayParams().getEventSkipCost();
            } else if (node.getState().getElementStatus() == StateElementStatus.ACTIVITY_SKIPPED) {
                mmCost += replayer.getReplayParams().getActivitySkipCost();
                mmUpper += replayer.getReplayParams().getActivitySkipCost();
            } else if (node.getState().getElementStatus() == StateElementStatus.ACTIVITY_MATCHED) {
                mSyncCost += replayer.getReplayParams().getActivityMatchCost();
                mmUpper += replayer.getReplayParams().getActivitySkipCost();
            }
            node = node.getParent();
        }

        //The original trace might not be played fully, consider all remaining events as EVENT_SKIP
        if (backtrackingNode != null && !backtrackingNode.getState().isTraceFinished()) {
            mlCost += (backtrackingNode.getState().getTrace().size() - backtrackingNode.getState().getTraceIndex())
                    * replayer.getReplayParams().getEventSkipCost();
        }
        this.isFitnessMetricsCalculated = true;
    }

    public double getMoveCostOnModelOnly() {
        if (!isFitnessMetricsCalculated) {
            this.calcFitnessMetrics();
        }
        return mmCost;
    }

    public double getMoveCostOnLogOnly() {
        if (!isFitnessMetricsCalculated) {
            this.calcFitnessMetrics();
        }
        return mlCost;
    }

    public double getSyncMoveCost() {
        if (!isFitnessMetricsCalculated) {
            this.calcFitnessMetrics();
        }
        return mSyncCost;
    }

    public double getUpperMoveCostOnLog() {
        if (!isFitnessMetricsCalculated) {
            this.calcFitnessMetrics();
        }
        return mlUpper;
    }

    public double getUpperMoveCostOnModel() {
        if (!isFitnessMetricsCalculated) {
            this.calcFitnessMetrics();
        }
        return mmUpper;
    }

    /**
     * Get trace fitness
     * @param minBoundMoveCostOnModel: the minimum move on model in event of no activity matches
     * @return 
     */
    public double getTraceFitness(double minBoundMoveCostOnModel) {
        this.calcFitnessMetrics();
        return (1 - (mmCost + mlCost + mSyncCost) / (mlUpper + minBoundMoveCostOnModel));
    }

    public void calcTiming() {
        if (this.replayer.getReplayParams().isBacktrackingDebug()) {
            LOGGER.info("REPLAYED TRACE BEFORE TIMING CALC");
            this.print();
        }

        calcTimingAll();

        if (this.replayer.getReplayParams().isBacktrackingDebug()) {
            LOGGER.info("REPLAYED TRACE AFTER TIMING CALC");
            this.print();
        }

        calculateCompleteTimestamp();
    }

    /*
    * timeOrderedReplayedNodes keeps the trace node in replay order. Note that
    * replay order follows the order of the trace event, so they are in ascending timing order.
    * In addition, in this order, the split gateway is always after the joining gateway and their
    * branch nodes are all in between them.
    * Use the flatten form of replayed trace to calculate timing for forks and joins
    * From a fork on the flatten trace, traverse forward and backward to search for 
    * a timed node (node with real timing data). Remember there is always either a timed start event 
    * or a timed activity or end event (also timed) as two ends on the traversing direction
    * Calculate in timing order from start to end
    */
    private void calcTimingAll() {
        TraceNode node;
        DateTime timeBefore = null;
        DateTime timeAfter = null;
        int timeBeforePos = 0;
        int timeAfterPos = 0;
        long duration;

        for (int i = 0; i < timeOrderedReplayedNodes.size(); i++) {
            node = timeOrderedReplayedNodes.get(i);
            timeBefore = null;
            timeAfter = null;
            if (!node.isTimed()) {

                //----------------------------------------
                // The incoming nodes to this node have been already assigned timestamps 
                // according to the traversal order starting from the Start event node (always timed)
                // Therefore, a node selects timestamp based on those of its incoming nodes.
                // This is to ensure a node's timestamp must be after all timestamp of its incoming nodes
                //----------------------------------------
                TraceNode mostRecentIncomingNode = null;
                for (TraceNode incomingNode : node.getSources()) {
                    if (timeBefore == null || timeBefore.isBefore(incomingNode.getStart())) {
                        timeBefore = incomingNode.getStart();
                        mostRecentIncomingNode = incomingNode;
                    }
                }
                timeBeforePos = timeOrderedReplayedNodes.indexOf(mostRecentIncomingNode);
                //----------------------------------------
                //Go backward and look for a timed node, 
                //known that it can either encounter a timed activity/gateway or 
                //the Start event node (always timed)
                //----------------------------------------
                /*
                for (int j=i-1;j>=0;j--) {
                if (timeOrderedReplayedNodes.get(j).isTimed()) {
                    timeBefore = timeOrderedReplayedNodes.get(j).getStart();
                    timeBeforePos = j;
                    break;
                }
                }
                */

                //----------------------------------------
                // Go forward and look for a timed node, known that it can encounter
                // either a timed node or the End event node eventually (always timed).
                // In the timeOrderedReplayedNodes array order, all nodes after 
                // this node are either subsequent and connected to it on the model or
                // in parallel with it. It is possible to set a node's timestamp 
                // after that of a parallel node because its next sequential node may have timestamp 
                // after the node in parallel. So, this ensures its timestamp is after 
                // any next node in chronological order, either sequential or parallel
                //----------------------------------------
                for (int j = i + 1; j < timeOrderedReplayedNodes.size(); j++) {
                    if (timeOrderedReplayedNodes.get(j).isTimed()
                            && timeOrderedReplayedNodes.get(j).getStart().isAfter(timeBefore)) {
                        timeAfter = timeOrderedReplayedNodes.get(j).getStart();
                        timeAfterPos = j;
                        break;
                    }
                }

                //----------------------------------------
                //It may happen that timeBefore >= timeAfter because the two activities
                //at timeBeforePos and timeAfterPos are on parallel branches and have 
                //the same timestamp.
                //----------------------------------------
                /*
                if (timeBefore != null && timeAfter != null && timeBefore.isEqual(timeAfter) && 
                   !node.getSources().contains(timeOrderedReplayedNodes.get(timeBeforePos))) {
                //For activity or split gateway: continue searching backward for another timed node
                if (node.getSources().size() <= 1) { 
                    for (int j=timeBeforePos-1;j>=0;j--) {
                        if (timeOrderedReplayedNodes.get(j).isTimed() &&
                            timeOrderedReplayedNodes.get(j).getStart().isBefore(timeAfter)) {
                            timeBefore = timeOrderedReplayedNodes.get(j).getStart();
                            timeBeforePos = j;
                            break;
                        }
                    }
                }
                //For joining gateway: continue searching forward for another timed node
                else {  
                    for (int j=timeAfterPos+1;j<timeOrderedReplayedNodes.size();j++) {
                        if (timeOrderedReplayedNodes.get(j).isTimed() && 
                            timeOrderedReplayedNodes.get(j).getStart().isAfter(timeBefore)) {
                            timeAfter = timeOrderedReplayedNodes.get(j).getStart();
                            timeAfterPos = j;
                            break;
                        }
                    }
                }
                }
                */

                //----------------------------------------------
                //Always take two ends of the trace plus a buffer as time limit
                //in case the replay trace has no timestamped activity at two ends 
                //NOTE: This is in case some process models cannot reach the End Event (unsound model)
                //----------------------------------------------
                if (timeBefore == null) {
                    timeBefore = (new DateTime(LogUtility.getTimestamp(logTrace.getTrace().get(0))))
                            .minusSeconds(replayer.getReplayParams().getStartEventToFirstEventDuration());
                }
                if (timeAfter == null) {
                    timeAfter = (new DateTime(
                            LogUtility.getTimestamp(logTrace.getTrace().get(logTrace.getTrace().size() - 1))))
                                    .plusSeconds(replayer.getReplayParams().getLastEventToEndEventDuration());
                }

                //----------------------------------------------
                // Take average timestamp between TimeBefore and TimeAfter
                //----------------------------------------------
                duration = (new Duration(timeBefore, timeAfter)).getMillis();
                if (timeAfterPos > timeBeforePos) {
                    duration = Math.round(1.0 * duration * (i - timeBeforePos) / (timeAfterPos - timeBeforePos));
                }
                node.setStart(timeBefore.plus(Double.valueOf(duration).longValue()));
            }
        }
    }

    /*
    private BidiMap<TraceNode,TraceNode> getForkJoinMap() {
    if (forkJoinMap.isEmpty()) {
        forkJoinMap = (new ForkJoinMap(this.startNode)).getFork2JoinMap();
    }
    return forkJoinMap;
    }
    */

    /*
    * Set complete timestamp for every node since event log only contains one timestamp
    * By default event timestamp is set to start date of a trace node
    * The complete date is calculated by adding to the start date 10% the duration 
    * from start date to the earliest date of all target nodes
    * Assume that all nodes have start timestamp calculated and assigned (not null).
    */
    private void calculateCompleteTimestamp() {
        DateTime earliestTarget = null;
        long transferDuration;
        for (TraceNode node : this.timeOrderedReplayedNodes) {
            if (node.isActivity()) {
                if (node.getTargets().size() > 0) {
                    earliestTarget = node.getTargets().get(0).getStart();
                    for (TraceNode target : node.getTargets()) {
                        if (earliestTarget.isAfter(target.getStart())) {
                            earliestTarget = target.getStart();
                        }
                    }
                    transferDuration = (new Duration(node.getStart(), earliestTarget)).getMillis();
                    node.setComplete(node.getStart()
                            .plusMillis(Long.valueOf(Math.round(transferDuration * 0.1)).intValue()));
                } else {
                    node.setComplete(node.getStart().plusMillis(5000));
                }
            } else {
                node.setComplete(node.getStart());
            }

        }
    }

    /*
    * Require a process model with block structure 
    * ANDsplit-ANDjoin, ORsplit-ORjoin, or ANDsplit-ORjoin
    * Note that ORsplit-ANDjoin creates an unsound model and should not exist
    */
    class ForkJoinMap {
        BidiMap<TraceNode, TraceNode> f2j = new DualHashBidiMap();
        Set visited = new HashSet(); //used to stop the recursive call chain as well as multiple visits to the same join gate
        Stack<TraceNode> s = new Stack();
        TraceNode firstElement = null;

        public ForkJoinMap(TraceNode firstElement) {
            this.firstElement = firstElement;
            depthfirst(firstElement);
        }

        private void depthfirst(TraceNode node) {
            if (!visited.contains(node)) {
                visited.add(node);

                if (node.isFork() || node.isORSplit()) {
                    s.push(node);
                } else if (node.isJoin() || node.isORJoin()) {
                    if (!s.isEmpty()) { // check stack in case of join node without split node (non-block structure)
                        f2j.put(s.pop(), node);
                    }
                }
                for (TraceNode nextNode : node.getTargets()) {
                    depthfirst(nextNode);
                }
            }
        }

        public BidiMap<TraceNode, TraceNode> getFork2JoinMap() {
            return f2j;
        }
    }

    /*
     * Print this trace to log in a hierarchical view
     */
    public void print() {
        int totalIndent = 0;
        int addedIndent = 0;
        FlowNode flowNode;
        Map<String, Integer> nodeTypeIndentMap = new HashMap();
        int counterDecision = 0;
        int counterMerge = 0;
        int counterFork = 0;
        int counterJoin = 0;
        int counterORSplit = 0;
        int counterORJoin = 0;

        for (TraceNode node : this.timeOrderedReplayedNodes) {
            String nodeString = "";
            String nodeType = "";
            String branchNodes = "";
            String dateString = "";
            addedIndent = 0;

            //------------------------------------
            // Print current node
            //------------------------------------
            if (node.isActivity()) {
                nodeType = "Activity";
                branchNodes = "";
            } else if (node.isStartEvent()) {
                nodeType = "StartEvent";
            } else if (node.isEndEvent()) {
                nodeType = "EndEvent";
            } else if (node.isFork()) {
                nodeType = "Fork";
                counterFork++;
                branchNodes += " => ";
                for (SequenceFlow flow : node.getOutgoingSequenceFlows()) {
                    flowNode = (FlowNode) flow.getTargetRef();
                    branchNodes += flowNode.getName();
                    branchNodes += "+";
                }
                addedIndent += 2;
                nodeTypeIndentMap.put(nodeType + counterFork, totalIndent);
            } else if (node.isJoin()) {
                nodeType = "Join";
                counterJoin++;
                branchNodes += " <= ";
                for (SequenceFlow flow : node.getIncomingSequenceFlows()) {
                    flowNode = (FlowNode) flow.getSourceRef();
                    branchNodes += flowNode.getName();
                    branchNodes += "+";
                }
                if (nodeTypeIndentMap.containsKey("Fork" + counterJoin)) {
                    totalIndent = nodeTypeIndentMap.get("Fork" + counterJoin).intValue();
                } else {
                    addedIndent -= 2;
                }
            } else if (node.isDecision()) {
                nodeType = "Decision";
                counterDecision++;
                branchNodes += " -> ";
                for (SequenceFlow flow : node.getOutgoingSequenceFlows()) {
                    flowNode = (FlowNode) flow.getTargetRef();
                    branchNodes += flowNode.getName();
                    branchNodes += "+";
                }
                addedIndent += 2;
                nodeTypeIndentMap.put(nodeType + counterDecision, totalIndent);
            } else if (node.isMerge()) {
                nodeType = "Merge";
                counterMerge++;
                branchNodes += " <- ";
                for (SequenceFlow flow : node.getIncomingSequenceFlows()) {
                    flowNode = (FlowNode) flow.getSourceRef();
                    branchNodes += flowNode.getName();
                    branchNodes += "+";
                }
                if (nodeTypeIndentMap.containsKey("Decision" + counterMerge)) {
                    totalIndent = nodeTypeIndentMap.get("Decision" + counterMerge).intValue();
                } else {
                    addedIndent -= 2;
                }
            } else if (node.isORSplit()) {
                nodeType = "OR-Split";
                counterORSplit++;
                branchNodes += " => ";
                for (SequenceFlow flow : node.getOutgoingSequenceFlows()) {
                    flowNode = (FlowNode) flow.getTargetRef();
                    branchNodes += flowNode.getName();
                    branchNodes += "+";
                }
                addedIndent += 2;
                nodeTypeIndentMap.put(nodeType + counterORSplit, totalIndent);
            } else if (node.isORJoin()) {
                nodeType = "OR-Join";
                counterORJoin++;
                branchNodes += " <= ";
                for (SequenceFlow flow : node.getIncomingSequenceFlows()) {
                    flowNode = (FlowNode) flow.getSourceRef();
                    branchNodes += flowNode.getName();
                    branchNodes += "+";
                }
                if (nodeTypeIndentMap.containsKey("OR-Split" + counterORJoin)) {
                    totalIndent = nodeTypeIndentMap.get("OR-Split" + counterORJoin).intValue();
                } else {
                    addedIndent -= 2;
                }
            }

            if (node.getStart() != null) {
                dateString = (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(node.getStart().toDate());
            }

            nodeString += (nodeType + ":" + node.getName() + ":" + dateString + ":" + branchNodes);
            nodeString = this.padLeft(nodeString, totalIndent);

            LOGGER.info(nodeString);
            totalIndent += addedIndent;
        }

    }

    private String padLeft(String s, int n) {
        if (n <= 0)
            return s;
        int noOfSpaces = n * 2;
        StringBuilder output = new StringBuilder(s.length() + noOfSpaces);
        while (noOfSpaces > 0) {
            output.append(" ");
            noOfSpaces--;
        }
        output.append(s);
        return output.toString();
    }

}