All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.smartclient.debug.public.sc.client.application.Workflow.js Maven / Gradle / Ivy

The newest version!
/*
 * Isomorphic SmartClient
 * Version SC_SNAPSHOT-2011-08-08 (2011-08-08)
 * Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
 * "SmartClient" is a trademark of Isomorphic Software, Inc.
 *
 * [email protected]
 *
 * http://smartclient.com/license
 */




// --------------------------------------------------------------------------------------------
//> @class ProcessElement
// A ProcessElement is an abstract superclass for elements involved in a +link{Process}, such
// as a +link{Task} or +link{XORGateway}.
// @visibility workflow
//<
isc.defineClass("ProcessElement");

isc.ProcessElement.addProperties({
    //> @attr processElement.ID (String : null : IR)
    // Optional ID for this process element, allowing it to be referred to from
    // +link{Gateway}s, or as the +link{process.startElement}.  See +link{ProcessSequence} and
    // +link{Process} to understand when this is required or can be omitted.
    // 

// Unlike +link{Canvas.ID} a processElement's is a not a globally unique // variable, it need only by unique within it's process. //

// When assigned an ID, a processElement can be retrieve via // +link{process.getElement()}. // @visibility workflow //< //> @attr processElement.nextElement (String : null : IR) // Next +link{process.sequences,sequence} or +link{process.elements,element} to execute // after this one completes. Sequences are checked first. nextElement does // not need to be specified on most elements if you use // +link{Process.sequences,sequences}. // @visibility workflow //< }); // -------------------------------------------------------------------------------------------- //> @class ProcessSequence // An Array of +link{ProcessElement}s involved in a +link{Process}. A // ProcessSequence is used to reduce the number of explicit // +link{ProcessElement.ID}s that need to be assigned, by creating an implicit next element - // the next in the sequence. //

// A sequence cannot be executed outside of a Process and has no state. // @visibility workflow //< isc.defineClass("ProcessSequence", "ProcessElement"); isc.ProcessSequence.addProperties({ //> @attr processSequence.elements (Array of ProcessElement : null : IR) // The +link{ProcessElement}s in this sequence. // @visibility workflow //< }); // -------------------------------------------------------------------------------------------- //> @class Task // A Task is an abstract superclass for +link{Process} and for all Task types that can be // involved in a Process, such as a +link{ServiceTask}. // // @visibility workflow //< isc.defineClass("Task", "ProcessElement"); isc.Task.addProperties({ //> @attr task.inputField (String : null : IR) // Field in the +link{Process.state,process state} which is provided as input data to this // task. // See +link{group:taskIO}. // @visibility workflow //< //> @attr task.inputFieldList (Array of String : null : IR) // List of multiple fields from the +link{Process.state,process state} which are provided // as input data to this task. See +link{group:taskIO}. //

// If +link{inputField} is also specified, it will be implicitly added to the // inputFieldList if it is not already present. // @visibility workflow //< //> @attr task.outputField (String : null : IR) // Field in the +link{Process.state,process state} which this task writes outputs to. See // +link{group:taskIO}. // @visibility workflow //< //> @attr task.outputFieldList (Array of String : null : IR) // List of multiple fields from the +link{Process.state,process state} which this task will // write to. See +link{group:taskIO}. //

// If +link{outputField} is also specified, it will be implicitly added to the // outputFieldList if it is not already present. // @visibility workflow //< //> @groupDef taskIO // Each task has inputs, which can be thought of as copied from the // +link{process.state,Process state} when the task is started, and outputs, which can be // thought of as atomically applied to the Process state when a task is completed. //

// Task can use +link{task.inputField} to specify the field from the Process state that // should be used as inputs, and +link{task.outputField} to specify the field from the // Process state that the task should output to. //

// More complex tasks can take multiple fields from the process state via // +link{task.inputFieldList} and write to multiple fields of the process state via // +link{task.outputFieldList}. In this case, the task is said to have an "input Record" // and/or "output Record", which can be thought of as a copy of the process state Record // with only the fields listed in the inputFieldList are copied. //

// When both inputField and inputFieldList are specified, the // inputField is considered the "primary" input field and will be used automatically by // various Task subclasses. // @title Task Input / Output // @visibility workflow //< //> @groupDef taskInputExpression // In some tasks, the input to the task needs to be passed to a service being called by the // task, to a user-visible form, or other consumers of the input data. // A TaskInputExpression can be used to do this declaratively. //

// A TaskInputExpression is a String prefixed with either "$input" or "$inputRecord", // followed by an optional dot-separated hierarchical path, which can specify either an // atomic data value (String, Number) or Record from the input data. For example, if the // +link{process.state} represented in JSON were: //

    // {
    //    orderId:5,
    //    orderItems: [
    //       {name:"Pencils", quantity:3, itemId:2344}
    //    ],
    //    orderUser: { name:"Henry Winkle", address:"...", ... }
    // }
    // 
// .. and a task specified an inputField of "orderId" and an inputFieldList of // "orderItems","orderUser", then: //
    //
  • $input is the value 5 //
  • $inputRecord.orderUser.name is "Henry Winkle" //
  • $inputRecord.orderItems[0] is the first orderItems Record ({name:"Pencils", ... }) //
// @title Task Input Expressions // @visibility workflow //< }); // -------------------------------------------------------------------------------------------- //> @class Process // A instance of Process represents a stateful process executing a series of Tasks, // which may be: //
    //
  • user interactions //
  • calls to DataSources (hence: any database or web service) //
  • arbitrary code //
  • other Processes //
// A Process is stateful in the sense that it maintains +link{process.state,state} // across the different tasks that are executed. This allows you to maintain context as you // walk a user through a multi-step business process in your application, which may involve // multiple operations on multiple entities. Each Task that executes can use the Process state // as inputs, and can output a result which is stored in the Process state - see // +link{group:taskIO}. //

// A Process can have multiple branches, choosing the next Task to execute based on // +link{Criteria} - see +link{XORGateway} and +link{DecisionGateway}. //

// Because a Process may return to a previous Task in various situations, the data model of a // Process is strictly speaking a graph (a set of nodes connected by arbitary // interlinks). However, most processes have sequences of several tasks in a row, and the // definition format allows these to be represented as simple Arrays called "sequences", // specified via +link{process.sequences}. This reduces the need to manually specify IDs and // interlinks for Tasks that simply proceed to the next task in a sequence. // // @visibility workflow //< isc.defineClass("Process", "Task"); isc.Process.addProperties({ init : function () { var res = this.Super("init", arguments); if (this.autoStart) { this.start(); } return res; }, //> @attr process.sequences (Array of ProcessSequence : null : IR) // Sequences of ProcessElements. By defining a sequences of elements you can make the // +link{processElement.nextElement} implicit. //

// You do not have to explicitly create a +link{ProcessSequence}, // you can instead use the shorthand: //

    // isc.Process.create({
    //     startElement:"firstSequence", 
    //     sequences: [
    //         { ID:"something", elements: [ ... ] },
    //         { ID:"somethingElse", elements: [ ... ] },
    //         ...
    //     ]
    //     ...
    // });
    // 
// .. this is equivalent to .. //
    // isc.Process.create({
    //     startElement:"firstSequence", 
    //     sequences: [
    //         isc.ProcessSequence.create({ 
    //              ID:"something", 
    //              elements: [ ... ] 
    //         }),
    //         isc.ProcessSequence.create({ 
    //              ID:"somethingElement", 
    //              elements: [ ... ] 
    //         }),
    //         ...                           
    //     ]
    //     ...
    // });
    // 
// // @visibility workflow //< //> @attr process.elements (Array of ProcessElement : null : IR) // Elements involved in this Process. You can also group elements into +link{sequences} // to reduce the need to explicitly define IDs for elements and interlink them. // @visibility workflow //< //> @attr process.startElement (String : null : IR) // The ID of either a +link{sequences,sequence} or an +link{elements,element} which should // be the starting point of the process. If not specified, the first sequence is chosen, // or if there are no sequences, the first element. // - log a warning and do nothing if there are neither sequences or elements // // - an example of how a Process would be defined // isc.Process.create({ // startElement:"firstSequence", // sequences: [ // { // id:"firstSequence", // elements : [ // isc.ServiceTask.create({ .. }), // isc.DecisionGateway.create({ .. }) // ] // }, // { // id:"errorFlow", // elements : [ ... ] // // } // ], // elements: [ // // standalone process elements not part of sequences // isc.ServiceTask.create({ .. }) // ], // state : { // someField:"someValue" // } // }) // @visibility workflow //< //> @method process.getElement() // Retrieve a +link{ProcessElement} by it's ID // @param ID (String) id of the process element // @return (ProcessElement) the indicated process element, or null if no such element // exists // @visibility workflow //< getElement : function (ID) { return this.searchElement(this, ID); }, searchElement : function (sequence, ID) { if (sequence.sequences != null) { for (var i = 0; i < sequence.sequences.length; i++) { var s = sequence.sequences[i]; if (s.ID == ID) { return s; } else if (s.sequences != null || s.elements != null) { var res = this.searchElement(s, ID); if (res != null) { return res; } } } } if (sequence.elements != null) { for (var i = 0; i < sequence.elements.length; i++) { var e = sequence.elements[i]; if (e.ID == ID) { return e; } else if (e.sequences != null || e.elements != null) { var res = this.searchElement(e, ID); if (res != null) { return res; } } } } }, //> @attr process.state (Record : null : IRW) // Current state of a process. As with Records in general, any field of a Record may // contain a nested Record or Array of Records, so the process state is essentially a // hierarchical data structure. // // @visibility workflow //< //> @attr process.autoStart (boolean : false : IR) // Cause the process to automatically call +link{start()} as soon as it is created. //< autoStart: false, //> @method process.start() // Starts this task by executing the +link{startElement}. //< start : function () { // Process can be async, so we continue it's execution no matter where we've stopped if (this.executionStack == null) { this.executionStack = []; } while (this.next() != null) { var currentTask = this.getFirstTask(); if (currentTask == null) { // empty sequence continue; } if (currentTask.getClassName() == "ScriptTask") { if (!this.executeScriptTaskElement(currentTask)){ return; } } else if (currentTask.getClassName() == "ServiceTask") { this.executeServiceTaskElement(currentTask) return; } } this.finished(); }, //> @method process.finished() // StringMethod called when a process completes, meaning the process executes a // ProcessElement with no next element. // @param state (Record) the final process state //< finished : function () { }, // If user didn't set ID or don't use nextElement property we will take next element // or sequence based on their order next : function () { var currEl = this.executionStack.last(); if (currEl == null) { // start processing if (this.startElement != null) { return this.gotoElement(this, this.startElement); } else if (this.sequences != null && this.sequences.length > 0) { this.executionStack.add({el:this, sIndex: 0}); return this.sequences[0]; } else if (this.elements != null && this.elements.length > 0) { this.executionStack.add({el:this, eIndex: 0}); return this.elements[0]; } else { isc.logWarn("There are neither sequences or elements. Nothing to execute."); } } else { var el = null; if (currEl.sIndex != null) { el = currEl.el.sequences[currEl.sIndex]; } else if (currEl.eIndex != null) { el = currEl.el.elements[currEl.eIndex]; } if (el.nextElement != null) { this.executionStack = []; var res = this.gotoElement(this, el.nextElement); return res; } else { return this.findNextElement(); } } }, gotoElement : function (sequence, ID) { var elData = {el: sequence}; this.executionStack.add(elData); if (sequence.sequences != null) { for (var i = 0; i < sequence.sequences.length; i++) { var s = sequence.sequences[i]; elData.sIndex = i; if (s.ID == ID) { return s; } else if (s.sequences != null || s.elements != null) { var res = this.gotoElement(s, ID); if (res != null) { return res; } } } } delete elData.sIndex; if (sequence.elements != null) { for (var i = 0; i < sequence.elements.length; i++) { var e = sequence.elements[i]; elData.eIndex = i; if (e.ID == ID) { return e; } else if (e.sequences != null || e.elements != null) { var res = this.gotoElement(e, ID); if (res != null) { return res; } } } } this.executionStack.removeAt(this.executionStack.length - 1); }, findNextElement : function () { var elData = this.executionStack.last(); if (elData.sIndex != null) { if (elData.sIndex == elData.el.sequences.length - 1) { this.executionStack.removeAt(this.executionStack.length - 1); if (elData.el == this) { return; } else { return this.findNextElement(); } } else { elData.sIndex++; return elData.el.sequences[elData.sIndex]; } } if (elData.eIndex != null) { if (elData.eIndex == elData.el.elements.length - 1) { this.executionStack.removeAt(this.executionStack.length - 1); if (elData.el == this) { return; } else { return this.findNextElement(); } } else { elData.eIndex++; return elData.el.elements[elData.eIndex]; } } }, // recursively search for first non-sequence in element getFirstTask : function () { var lastElData = this.executionStack.last(); var el = null; if (lastElData.sIndex != null) { el = lastElData.el.sequences[lastElData.sIndex]; } else if (lastElData.eIndex != null) { el = lastElData.el.elements[lastElData.eIndex]; } if (el.sequences == null && el.elements == null) { return el; } var elData = {el: el}; this.executionStack.add(elData); if (el.sequences != null) { for (var i = 0; i < el.sequences.length; i++) { elData.sIndex = i var res = this.getFirstTask(el.sequences[i]); if (res != null) { return res; } } } if (el.elements != null) { for (var i = 0; i < el.elements.length; i++) { elData.eIndex = i var res = this.getFirstTask(el.elements[i]); if (res != null) { return res; } } } this.executionStack.removeAt(this.executionStack.length - 1); }, executeScriptTaskElement : function (element) { // process input var inputData; var inputRecord; if (element.inputFieldList != null) { inputRecord = {}; for (var i = 0; i < element.inputFieldList.length; i++) { inputRecord[element.inputFieldList[i]] = this.state[element.inputFieldList[i]]; }; } if (element.inputField != null) { inputData = this.state[element.inputField]; if (inputRecord != null) { inputRecord[element.inputField] = inputData; } } element.inputData = inputData; element.inputRecord = inputRecord; element.process = this; try { var output = element.execute(inputData, inputRecord); } catch (e) { isc.logWarn("Error while executing ScriptTask: "+e.toString()); } if (element.isAsync) { return false; } if (typeof output == 'undefined') { return true; } this.processTaskOutput(element, output); return true; }, processTaskOutput : function (task, output) { // process output if (task.outputFieldList != null) { for (var i = 0; i < task.outputFieldList.length; i++) { if (typeof output[task.outputFieldList[i]] != 'undefined') { this.state[task.outputFieldList[i]] = output[task.outputFieldList[i]]; } }; } if (task.outputField != null) { if (task.outputFieldList == null) { if (typeof output != 'undefined') { this.state[task.outputField] = output; } } else { if (typeof output[task.outputField] != 'undefined') { this.state[task.outputField] = output[task.outputField]; } } } }, finishTask : function (task, outputRecord, outputData) { // ProcessTask use the method to continue process if (outputRecord == null) { this.processTaskOutput(task, outputData); } else { if (outputData != null) { outputRecord[task.outputField] = outputData; } this.processTaskOutput(task, outputRecord); } if (task.isAsync) { this.start(); } }, executeServiceTaskElement : function (task) { var ds = task.dataSource; if (ds.getClassName == null || ds.getClassName() != "DataSource") { ds = isc.DataSource.get(ds); } var inputData = {}; if (task.inputFieldList != null) { for (var i = 0; i < task.inputFieldList.length; i++) { inputData[task.inputFieldList[i]] = this.state[task.inputFieldList[i]]; }; } if (task.inputField != null) { inputData[task.inputField] = this.state[task.inputField]; } var data = null; if (task.operationType == "fetch") { if (task.criteria != null) { data = task.criteria; this.processCriteriaExpressions(data, task, inputData); } if (task.fixedCriteria != null) { if (data == null) { data = task.fixedCriteria } else { data = isc.DataSource.combineCriteria(data, task.fixedCriteria); } } } if (data == null) { data = inputData; } var process = this; ds.performDSOperation(task.operationType, data, function(dsResponse) { if (dsResponse.data.length == 1) { if (task.outputFieldList != null) { for (var i = 0; i < task.outputFieldList.length; i++) { if (typeof dsResponse.data[0][task.outputFieldList[i]] != 'undefined') { process.state[task.outputFieldList[i]] = dsResponse.data[0][task.outputFieldList[i]]; } }; } if (task.outputField != null) { process.state[task.outputField] = dsResponse.data[0][task.outputField]; } } process.start(); }); }, processCriteriaExpressions : function (criteria, task, inputData) { for (var name in criteria) { if (name == "criteria") { this.processCriteriaExpressions(criteria.criteria); } else if (criteria[name].startsWith("$input")) { var script = "state." + criteria[name].replace("$input", task.inputField); criteria[name] = isc.Class.evaluate(script, {state: inputData}); } else if (criteria[name].startsWith("$inputRecord")) { var script = criteria[name].replace("$inputRecord", "state"); criteria[name] = isc.Class.evaluate(script, {state: inputData}); } } } }); // -------------------------------------------------------------------------------------------- //> @class ServiceTask // A ServiceTask is an element of a +link{Process} which calls a DataSource operation, // optionally using part of the +link{Process.state,process state} as inputs or storing outputs // in the process state. //

// By default a ServiceTask takes the data indicated by +link{task.inputField} and uses it as // +link{dsRequest.data}. This means the input data becomes +link{Critera} for a "fetch" // operation, new record values for an "add" operation, etc. //

// Alternatively, you can set +link{serviceTask.criteria} for a "fetch" operation, or // +link{serviceTask.values} for other operationTypes. In both cases, you have the ability to // use simple expressions like $input.fieldName to take portions of the input data and // use it as part of the criteria or values. //

// As a special case, if the inputField is an atomic value (just a String or // Number rather than a Record) and operationType is "fetch", it will be assumed to be value // for the primary key field of the target DataSource if +link{serviceTask.criteria,criteria} // is not explicitly specified // @visibility workflow //< isc.defineClass("ServiceTask", "Task"); isc.ServiceTask.addProperties({ //> @attr serviceTask.dataSource (DataSource or identifier : null : IR) // DataSource ID or DataSource instance to be used. // @visibility workflow //< //> @attr serviceTask.operationType (DSOperationType : "fetch" : IR) // Type of operation to invoke // @visibility workflow //< operationType: "fetch" //> @attr serviceTask.criteria (Criteria : null : IR) // Criteria (including AdvancedCriteria) to use for a "fetch" operation. //

// Data values in this criteria prefixed with "$" will be treated as dynamic expressions // which can access the inputs to this task as $input - see // +link{group:taskInputExpression}. Specifically, this means that for simple criteria, // any property value that is a String and is prefixed with "$" will be assumed to be an // expression, and for AdvancedCriteria, the same treatment will be applied to // +link{criterion.value}. //

// If any data value should not be treated as dynamic (for example, a "$" should be taken // as literal), you can place it in +link{fixedCriteria} instead. //

// Ignored for any operationType other than "fetch". Update or delete operations should // place the primary key to update in +link{values}. // @group taskIO // @visibility workflow //< //> @attr serviceTask.values (Record : null : IR) // Values to be submitted for "update", "add" and "remove" operations. //

// Similar to +link{criteria}, data values prefixed with "$" will be treated as a // +link{group:taskInputExpression}. Use +link{fixedValues} for any values that start with // "$" but should be treated as a literal. // @visibility workflow //< //> @attr serviceTask.fixedCriteria (Criteria : null : IR) // Criteria to be submitted as part of the DSRequest, regardless of inputs to the task. // Will be combined with the data from the +link{task.inputField} or with // +link{serviceTask.criteria} if specified, via +link{DataSource.combineCriteria()}. // @visibility workflow //< //> @attr serviceTask.fixedValues (Record : null : IR) // Values to be submitted as part of the DSRequest, regardless of inputs to the task. Will // be combined with the data from the +link{task.inputField} or with // +link{serviceTask.values} if specified, via simple copying of fields, with // fixedValues overwriting values provided by the inputField, but // explicitly specified +link{serviceTask.values} overriding fixedValues. // @visibility workflow //< }); // -------------------------------------------------------------------------------------------- //> @class ScriptTask // Task that executes arbitrary code, either synchronous or asynchronous. Override the // +link{execute()} method to provide custom logic. // @visibility workflow //< isc.defineClass("ScriptTask", "Task"); isc.ScriptTask.addProperties({ //> @method scriptTask.getInputData() // Get the inputs to this task as specified by +link{task.inputField}. //

// For a task with a +link{task.inputFieldList,inputFieldList}, use +link{getInputRecord} // to get access to other inputs. // @return (Object) input data // @group taskIO // @visibility workflow //< getInputData : function () { return this.inputData; }, //> @method scriptTask.setOutputData() // Set the task output as specified by +link{task.outputField}. //

// NOTE: for an +link{script.isAsync,asychronous task}, calling // setOutputData() indicates the task is complete. For a task with // +link{task.outputFieldList,multiple outputs}, call +link{setOutputRecord()} instead. // @param (Object) task output // @group taskIO // @visibility workflow //< setOutputData : function (taskOutput) { this.process.finishTask(this, null, taskOutput); }, //> @method scriptTask.getInputRecord() // Get all inputs to the task as specified by the // +link{task.inputFieldList,inputFieldList}, as a Record. // @return (Record) input data // @group taskIO // @visibility workflow //< getInputRecord : function () { return this.inputRecord; }, //> @method scriptTask.setOutputRecord() // Set all outputs of the task as specified by the // +link{task.outputFieldList,outputFieldList}, by providing a Record. // @param (Record) task output // @group taskIO // @visibility workflow //< setOutputRecord : function (outputRecord) { this.process.finishTask(this, outputRecord); }, //> @attr scriptTask.isAsync (boolean : false : IR) // Whether the script task is asynchronous. A synchronous task is expected to return data // directly from execute() and is considered complete once the execute() method exits. //

// An asnychronous task is expected to start processing in execute(), and will not be // considered complete until either +link{setOutputData()} or +link{setOutputRecord} is // called. // @visibility workflow //< isAsync : false, //> @method scriptTask.execute() // Execute the task. // @param input (Object) the task input // @param inputRecord (Record) the task input record if an inputFieldList was // specified. See +link{group:taskIO} // @return (Object) the task output. For multiple field output, call // +link{setOutputRecord()} instead, and return null // @visibility workflow //< execute : function (input, inputRecord) { } }); // -------------------------------------------------------------------------------------------- //> @class XORGateway // Chooses one or another next process element based on AdvancedCriteria applied to // +link{process.state}. //

// If the AdvancedCriteria evaluate to true, the +link{nextElement} is chosen, otherwise the // +link{failureElement}. //

// Note that "XOR" in XORGateway means "exclusive or" - only one next element is // chosen. // - implementation note: we need to allow the propertyName in simple Criteria or the // criterion.name in AdvancedCriterion to a be path of the form "orderUser.name". This should // be a general enhancement applied across the entire Criteria/AdvancedCriteria system. // @visibility workflow //< isc.defineClass("XORGateway", "ProcessElement"); isc.XORGateway.addProperties({ //> @attr xorGateway.criteria (Criteria : IR : IR) // Simple or +link{AdvancedCriteria} to be applied to the task inputs. These will be // applied to either the data indicated by the +link{task.inputField} or to the // "inputRecord" if multiple input fields are declared (see +link{taskIO}). // @visibility workflow //< //> @attr xorGateway.nextElement (String : null : IR) // ID of the next +link{process.sequences,sequence} or {process.elements,element} to // procede to if the criteria match the process state. If this gateway is part of a // +link{process.sequences,sequence} and has a next element in the sequence, // nextElement does not need to be specified. // @visibility workflow //< //> @attr xorGateway.failureElement (String : null : IR) // ID of the next sequence or element to proceed to if the criteria do not match. // @visibility workflow //< }); // -------------------------------------------------------------------------------------------- //> @class DecisionGateway // Chooses a next element in a +link{Process} by evaluating a series of criteria against the // +link{process.state} and choosing the element associated with the criteria that matched, or // a +link{defaultElement} if none of the criteria match. // @visibility workflow //< isc.defineClass("DecisionGateway", "Task"); isc.DecisionGateway.addProperties({ //> @attr decisionGateway.criteriaMap (Object : null : IR) // A Map from +link{ProcessElement.ID} to Criteria that will cause this ProcessElement to // be chosen as the next element if the criteria matches. // @visibility workflow //< //> @attr decisionGateway.defaultElement (String : null : IR) // Next element to pick if no criteria match. If this gateway is part of a // +link{process.sequences,sequence} and has a next element in the sequence, the // defaultElement is assumed to be the next element and does not need to be // specified. // @visibility workflow //< });





© 2015 - 2024 Weber Informatics LLC | Privacy Policy