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