Name

global.WalkWorkflow

Description

This utility class walks activities in a workflow and generates either full or expected sequences. Sequences are segements of comma-separated lists of activity IDs. The segments are separated by a pipe character | If a sequence list ends in a | then it indicates that that sequence list was not completely generated, which happens for overly complicated workfllows.

Script

/**
* Walk the workflow and get the list of successors for each activity along with
* a shortest path order indicating how deep the activity is in the workflow
*/

/* global 
*   gs
*/
/* eslint-disable strict 
*/

gs.include("PrototypeServer");

var WalkWorkflow = Class.create();
WalkWorkflow.prototype = {
  MAX_SEQUENCE_COUNT : 2000,
  MAX_SEQUENCE_LENGTH : 100,

  initialize : function(/* GlideRecord */workflowVersion) {
  	this.workflowVersion = workflowVersion;
  },

  /**
   * Walk the workflow and set up the activity shortest path ordering
   */
  walk : function(/* boolean */fullSequences) {
  	this.activities = {};
  	this._getDependencyMap();
  	this._setShortestPathOrders();
  	if (fullSequences)
  		this.computeFullSequences();
  	else
  		this.computeSequences();
  },

  /**
   * Get all of the activities at the specified shortest path order
   * 
   * returns an array of activity sys_ids at the specified order if an empty
   * array is returned, there are no more activities
   */
  getActivitiesAtOrder : function(order) {
  	var ret = [];
  	for ( var id in this.activities) {
  		if (this.activities[id].order == order)
  			ret.push(id);
  	}
  	return ret;
  },

  /**
   * Get the activity sequences as a string of |-terminated comma-separated
   * lists of activities (walk() must have been called prior to calling this
   * method)
   */
  getSequences : function() {
  	var seqs = [];
  	var len = this.sequences.length;
  	for (var i = 0; i < len; i++)
  		seqs.push(this._getSequence(this.sequences[i]));

  	return seqs.join('|');
  },

  _getSequence : function(seq) {
  	var a = [];
  	var len = seq.length;
  	for (var i = 0; i < len; i++)
  		a.push(seq[i]);

  	return a.join(",");
  },

  _getDependencyMap : function() {
  	this.transitions = {};
  	var gr = new GlideRecord('wf_activity');
  	gr.addQuery('workflow_version', this.workflowVersion.sys_id);
  	gr.addNullQuery('parent');
  	gr.query();
  	while (gr.next()) {
  		var activity = new WalkWorkflowActivity(gr.sys_id.toString(),
  				gr.name.toString(), gr.activity_definition.attributes
  						.toString());
  		this.activities[activity.sys_id] = activity;
  	}

  	this._getChildren();

  	for ( var id in this.activities) {
  		var activity = this.activities[id];
  		this._getTransitions(activity);
  	}
  },

  _getChildren : function() {
  	// load up any child activities and associated them with their parent
  	var gr = new GlideRecord('wf_activity');
  	gr.initialize();
  	gr.addQuery('workflow_version', this.workflowVersion.sys_id);
  	gr.addNotNullQuery('parent');
  	gr.query();
  	while (gr.next()) {
  		var parent = this.activities[gr.parent.toString()];
  		if (!parent)
  			continue;

  		var activity = new WalkWorkflowActivity(gr.sys_id.toString(),
  				gr.name.toString(), gr.activity_definition.attributes
  						.toString());
  		parent.children.push(activity);
  	}
  },

  _getTransitions : function(activity) {
  	var sysId = activity.sys_id.toString();
  	var gr = new GlideRecord('wf_transition');
  	gr.addQuery('from', sysId);
  	gr.query();
  	while (gr.next()) {
  		if (gr.condition.skip_during_generate == true)
  			activity.tos[gr.to.toString()] = "skipped";
  		else
  			activity.tos[gr.to.toString()] = "not_skipped";
  	}
  },

  // Set shortest path order to each activity
  _setShortestPathOrders : function() {
  	this._setShortestPathOrder(this.workflowVersion.start, 0, {});
  },

  _setShortestPathOrder : function(sysId, order, visited) {
  	var activity = this.activities[sysId];
  	if (visited[sysId])
  		return;

  	visited[sysId] = true;
  	if (activity.order == 0)
  		activity.order = order;

  	order++;
  	for ( var to in activity.tos) {
  		this._setShortestPathOrder(to, order, visited);
  	}
  },

  /**
   * Compute the sequences of activities. All sequences leading up to a join
   * end at the join and then a single sequence from the join is computed.
   * This ensures that we can walk all sequences up to a join and then walk
   * the sequence from the join.
   * 
   * For example:
   * 
   * Begin -> T1 -> T1.1 -> Join1 -> T3 -> End -> T2 -------------^
   * 
   * would result in these sequences:
   * 
   * 1. Begin, T1, T1.1, Join1 2. Begin, T2, Join1 3. Join1, T3, End
   * 
   * and this:
   * 
   * Begin -> T1 -> T1.1 -> Join1 -------> T3 -> Join2 -> T5 -> End -> T2
   * -------------^ -> T4 -----^
   * 
   * would result in these sequences:
   * 
   * 1. Begin, T1, T1.1, Join1 2. Begin, T2, Join1 3. Join1, T3, Join2 4.
   * Join1, T4, Join2 5. Join2, T5, End
   */
  computeSequences : function() {
  	this.visited = {};
  	this.sequences = [];
  	this.sequenceKeys = {};
  	this.joins = [];
  	this.sequenceCount = 0;
  	this._computeSequence(this.workflowVersion.start, []);

  	var joinId;
  	this.joinNdx = 0;
  	this.joinsVisited = {};
  	while ((joinId = this._nextJoin()) != null)
  		this._computeSequence(joinId, []);

  	if (this.sequenceCount > this.MAX_SEQUENCE_COUNT) {
  		gs.print("Workflow " + this.workflowVersion.name
  				+ " - expected sequences list too large, not saved");
  		this._addSequence([]);
  		return;
  	}

  	this.sequenceKeys = null;
  	this.visited = null;
  	this.joins = null;
  	this.joinsVisited = null;
  },

  _computeSequence : function(id, sequence) {
  	if (sequence.length > this.MAX_SEQUENCE_LENGTH)
  		return;

  	var seq = sequence.slice(0);
  	this.visited[id] = true;

  	this.sequenceCount++;
  	if (this.sequenceCount > this.MAX_SEQUENCE_COUNT)
  		return;
  	seq.push(id);
  	var a = this.activities[id];
  	var end = true;
  	for ( var tid in a.tos) {
  		if (a.tos[tid] == "skipped")
  			continue;

  		end = false;
  		break;
  	}
  	if (end) {
  		this._addSequence(seq);
  		return;
  	}

  	for ( var to in a.tos) {
  		if (a.tos[to] == "skipped")
  			continue;

  		if (this.visited[to]) {
  			this._addSequence(seq.slice(0));
  			continue;
  		}

  		var toAct = this.activities[to];
  		if (!toAct.isJoin) {
  			this._computeSequence(to, seq);
  			if (this.sequenceCount > this.MAX_SEQUENCE_COUNT)
  				return;
  			continue;
  		}

  		// Add the join to our join list to process at the end
  		this.sequenceCount++;
  		if (this.sequenceCount > this.MAX_SEQUENCE_COUNT)
  			return;
  		seq.push(to);
  		this._addSequence(seq.slice(0));
  		this.joins.push(to);
  	}
  	return;
  },

  _addSequence : function(seq) {
  	// don't add duplicate sequences
  	var seqKey = seq.join(',');
  	if (this.sequenceKeys[seqKey])
  		return;

  	this.sequenceKeys[seqKey] = true;
  	this.sequences.push(seq);
  },

  _nextJoin : function() {
  	while (this.joinNdx < this.joins.length) {
  		var joinId = this.joins[this.joinNdx];
  		this.joinNdx++;
  		if (this.joinsVisited[joinId]) {
  			continue;
  		}

  		this.joinsVisited[joinId] = true;
  		return joinId;
  	}
  	return null;
  },

  computeFullSequences : function() {
  	this.sequenceCount = 0;
  	this.sequences = [];
  	this.sequenceKeys = {};
  	this._computeFullSequence(this.workflowVersion.start, []);
  	this.sequenceKeys = null;
  	if (this.sequenceCount > this.MAX_SEQUENCE_COUNT) {
  		gs.print("Workflow " + this.workflowVersion.name
  				+ " - full sequences list too large, not saved");
  		this._addSequence([]);
  	}
  },

  _computeFullSequence : function(id, sequence) {
  	if (this.sequenceCount > this.MAX_SEQUENCE_COUNT)
  		return;

  	if (sequence.length > this.MAX_SEQUENCE_LENGTH)
  		return;

  	var seq = sequence.slice(0);
  	var len = seq.length;
  	if (this._exists(seq, id)) {
  		// seen this one before on this sequence - do not loop, just save up
  		// to here
  		this._addSequence(seq);
  		return;
  	}

  	this.sequenceCount++;
  	if (this.sequenceCount > this.MAX_SEQUENCE_COUNT)
  		return;

  	seq.push(id);
  	var a = this.activities[id];
  	var end = true;
  	for ( var tid in a.tos) {
  		end = false;
  		break;
  	}
  	if (end) {
  		this._addSequence(seq);
  		return;
  	}

  	for ( var to in a.tos) {
  		if (this._exists(seq, to)) {
  			this._addSequence(seq.slice(0));
  			continue;
  		}

  		this._computeFullSequence(to, seq);
  	}
  	return;
  },

  // Useful debugging/dumping support

  dump : function() {
  	gs.print("Workflow: " + this.workflowVersion.name);
  	this.dumpActivities = {};
  	this._dump(this.workflowVersion.start, 0);
  	this.dumpActivities = null;
  	gs.print("");
  	gs.print("Sequences:");
  	var joins = {};
  	this.dumpSequences();
  	gs.print("");
  	gs.print("Shortest Paths:");
  	this.dumpShortestPathOrders();
  	gs.print("");
  	gs.print("");
  },

  dumpSequences : function() {
  	for (var i = 0; i < this.sequences.length; i++)
  		this._dumpSequence(this.sequences[i]);
  },

  _dumpSequence : function(seq) {
  	var a = [];
  	for (var i = 0; i < seq.length; i++)
  		a.push(this.activities[seq[i]].name);

  	var s = a.join(", ");
  	gs.print(s);
  },

  dumpShortestPathOrders : function() {
  	var order = 0;
  	var ret;
  	do {
  		ret = this.getActivitiesAtOrder(order);
  		for (var i = 0; i < ret.length; i++) {
  			var activity = this.activities[ret[i]];
  			gs.print(activity.order + ": " + activity.name);
  			for (var childNdx = 0; childNdx < activity.children.length; childNdx++)
  				gs.print("   : " + activity.children[childNdx].name);
  		}
  		order++;
  	} while (ret.length > 0);
  },

  _dump : function(sysId, order) {
  	if (this.dumpActivities[sysId])
  		return;

  	this.dumpActivities[sysId] = true;
  	var activity = this.activities[sysId];
  	if (activity.isJoin)
  		gs.print("Name: " + activity.name + "*");
  	else
  		gs.print("Name: " + activity.name);

  	order++;
  	for ( var to in activity.tos) {
  		var toActivity = this.activities[to];
  		gs.print("   To: " + toActivity.name);
  	}

  	for ( var to in activity.tos) {
  		this._dump(to, order);
  	}
  },

  _exists : function(arr, id) {
  	var len = arr.length;
  	for (var i = 0; i < len; i++) {
  		if (arr[i] == id)
  			return true;
  	}
  	return false;
  },

  type : 'WalkWorkflow'
}

/**
* This is the activity that is created by the WalkWorkflow class when walking a
* workflow and ordering the activities
*/
var WalkWorkflowActivity = Class.create();
WalkWorkflowActivity.prototype = {

  initialize : function(sysId, name, attributes) {
  	this.sys_id = sysId;
  	this.name = name;
  	this.attributes = attributes;
  	this.tos = {};
  	this.order = 0;
  	this.children = [];
  	this.isJoin = (attributes.indexOf("generate=join") != -1);
  },

  type : 'WalkWorkflowActivity'
}

Sys ID

441f22d40a2581027b72c63a41c520ff

Offical Documentation

Official Docs: