importPackage(Packages.de.elo.ix.client); //@include lib_Class.js /** * ELOas workflow controller. * * This class handles ELO AS functions. Functions must be a subclass of sol.common.as.FunctionBase. * * ELO AS functions can be used as workflow nodes. Compared to Index Server functions, there's no script that can be called. * Therefore a direct rule is called by this controller. The required rule must be passed as a param in the workflows node description field. * * # Sample node configuration: * * { * "$directRule": "sol.common.as.functions.MyFunction", * "doSth": "like this" * } * * # Controller functions * Instead of a direct rule, the `WfController` can use a `$controllerFunction`. * Currently only {@link #wait} is supported. * * # Forwarding behavior * If the controller calls a direct function, the return value of this function will affect the forwarding behavior. * * To forward to the next node (if there are more than one that will be the first one) the direct rule has to return * * { passOn: true } * * To prevent the forwarding the direct rule has to return * * { passOn: false } * * If all successor nodes should be activated the result has to be * * { passOn: { all: true } } * * If the forwarding should happen to specific successor nodes this could be defined by the following result * * { passOn: { nodes: [] } } * * The nodes list has to contain strings. Those could be node translation keys, names or ids. * * If the controller uses a `$controllerFunction` check the specific function for forwarding configuration (e.g. {@link #wait}). * * # Delay workflows (since 1.05.000) * To take some load of the `WfController` (in systems with a lot of active workflows) those wokflows could be delayed, and therefore the `WfController` ignores them. * * To activate this in general the node has do define a `useDelay` property and set that to `true`. * * There are several ways do specify the delay: * * - The controller function `wait` can define a check intervall (see {@link #wait}) * - The controller function `wait` can delay until the check date is reached (see {@link #wait}) * - A direct rule can define a return property `delayDateIso` to delay the next execution until that date * * **Delay the workflow with direct rule result** * * Node configuration: * * { * "$directRule": "sol.common.as.functions.MyFunction", * "useDelay": true * } * * Result of `sol.common.as.functions.MyFunction`: * * { passOn: false, delayDateIso: "20181231120000" } * * This will delay the next execution until 12 o'clock on December, 31 2018. * The delay is just applied if `passOn` is not `true` and if the node configuration defines `useDelay=true`. * * @author MW, ELO Digital Office GmbH * @version 1.05.000 * * @eloas * * @requires sol.common.Config * @requires sol.common.ObjectUtils * @requires sol.common.ExceptionUtils * @requires sol.common.StringUtils * @requires sol.common.RepoUtils * @requires sol.common.SordUtils * @requires sol.common.WfUtils * @requires sol.common.as.Utils * */ sol.define("sol.common.as.WfController", { singleton: true, requiredAsVersion: "9.03.006", solutionAsConfigs: [], /** * Runs workflow rules */ run: function () { var me = this, currentVersion, versionCheckResult, nodeConfig, resultObj, tplSord; if (!me.logger) { me.logger = sol.create("sol.Logger", { scope: "sol.common.as.WfController" }); } if (!me.versionChecked) { if (!sol.common.as.Utils.isDebugger()) { currentVersion = sol.common.as.Utils.getAsVersion(); versionCheckResult = sol.common.RepoUtils.checkVersion(currentVersion, me.requiredAsVersion); if (!versionCheckResult) { me.logger.warn("ELOas version " + me.requiredAsVersion + " or higher is required."); return; } } me.versionChecked = true; } me.logger.enter("WfController.run", { flowName: EM_WF_NODE.flowName + "", flowId: EM_WF_NODE.flowId + "", nodeName: EM_WF_NODE.nodeName + "" }); nodeConfig = me.getNodeConfig(); if (!nodeConfig) { me.logger.info(["Skipping node \"{0}\" (flowName={1}, flowId={2}). No configuration given.", EM_WF_NODE.nodeName, EM_WF_NODE.flowName, EM_WF_NODE.flowId]); me.logger.exit("WfController.run"); return; } if (!nodeConfig.$directRule && !nodeConfig.$controllerFunction) { me.logger.info(["Skipping node \"{0}\" (flowName={1}, flowId={2}). Node has no direct rule or controller function configuration.", EM_WF_NODE.nodeName, EM_WF_NODE.flowName, EM_WF_NODE.flowId]); me.logger.exit("WfController.run"); return; } if (nodeConfig.$directRule) { resultObj = me.handleDirectRule(nodeConfig); } if (me.logger.debugEnabled) { tplSord = sol.common.SordUtils.getTemplateSord(EM_ACT_SORD).sord; me.logger.debug("tplSord=" + JSON.stringify(tplSord)); } if (nodeConfig.$controllerFunction) { resultObj = me.handleControllerFunction(nodeConfig); } if (resultObj) { if (nodeConfig.debug) { EM_WF_NEXT = ""; me.logger.debug(["Debug mode - don't pass on: EM_WF_NODE = \"\""]); } else { me.dispatch(resultObj, nodeConfig); me.writeWfChanges(resultObj, nodeConfig); } } me.logger.exit("WfController.run"); }, /** * @private * Handle execution of direct rules * @param {Object} nodeConfig * @return {Object} */ handleDirectRule: function (nodeConfig) { var me = this, executionParams = {}, logExecutionParams = {}, wfDiagram, result, resultObj; if (!ruleExecutor.hasDirectRule(nodeConfig.$directRule)) { me.handleException("Direct rule '" + nodeConfig.$directRule + "' not found."); return; } wfDiagram = ixConnect.ix().checkoutWorkFlow(String(EM_WF_NODE.flowId), WFTypeC.ACTIVE, WFDiagramC.mbAll, LockC.NO); sol.common.WfUtils.checkMainAdminWf(wfDiagram); me.logger.debug(["Execute direct rule: directRule={0}, nodeName={1}, flowName={2}, flowId={3}", nodeConfig.$directRule, EM_WF_NODE.nodeName, EM_WF_NODE.flowName, EM_WF_NODE.flowId], nodeConfig); nodeConfig.EM_WF_NODE = { nodeName: String(EM_WF_NODE.nodeName), flowName: String(EM_WF_NODE.flowName), flowId: String(EM_WF_NODE.flowId), objId: String(EM_WF_NODE.objId), workflowOwnerName: String(EM_WF_NODE.workflowOwnerName) }; executionParams.ticket = ixConnect.loginResult.clientInfo.ticket + ""; logExecutionParams.ticket = executionParams.ticket.substr(0, 10) + "..."; executionParams.language = ixConnect.loginResult.clientInfo.language + ""; executionParams.timeZone = ixConnect.loginResult.clientInfo.timeZone + ""; if (!executionParams.timeZone) { executionParams.timeZone = java.time.ZoneId.systemDefault().toString() + ""; } result = ruleExecutor.runDirectRule(nodeConfig.$directRule, EM_ACT_SORD.id, JSON.stringify(nodeConfig), JSON.stringify(executionParams)); me.logger.debug(["Direct rule result: {0}", result]); if (!result || result == "undefined") { me.handleException("Direct rule result is empty"); return; } if (result) { if (result.indexOf("Ruleset not found") == 0) { me.handleException("Direct rule not found"); return; } try { resultObj = JSON.parse(result); } catch (ex) { me.handleException("Can't parse direct rule result"); return; } if (resultObj && resultObj.exception) { me.handleException("Direct rule exception: " + resultObj.exception); return; } return resultObj; } }, /** * @private * Handle execution of controller function * @param {Object} nodeConfig * @return {Object} */ handleControllerFunction: function (nodeConfig) { var me = this, controllerFunctionName, controllerFunction, resultObj; controllerFunctionName = nodeConfig.$controllerFunction; controllerFunction = me[controllerFunctionName]; if (!controllerFunction) { me.handleException("Controller function '" + controllerFunctionName + "' not found."); return; } me.logger.debug(["Execute controller function: controllerFunction={0}, nodeName={1}, flowName={2}, flowId={3}", controllerFunctionName, EM_WF_NODE.nodeName, EM_WF_NODE.flowName, EM_WF_NODE.flowId], nodeConfig); try { resultObj = controllerFunction.call(me, nodeConfig); } catch (ex) { me.handleException("Exception in controller function: " + sol.common.ExceptionUtils.parseException(ex)); return; } if (!resultObj) { me.handleException("Controller function '" + controllerFunctionName + "' must provide a result."); return; } me.logger.debug(["Controller function result: {0}", JSON.stringify(resultObj)]); return resultObj; }, /** * @private * Get node configuration * @return {Object} Node configuration */ getNodeConfig: function () { var me = this, nodeConfigString, nodeConfig, jsonEndPos, properties, comment; properties = EM_WF_NODE.properties + ""; comment = EM_WF_NODE.nodeComment + ""; nodeConfigString = properties || comment; try { if (nodeConfigString) { if ((nodeConfigString.length > 1) && (nodeConfigString.charAt(0) == "{")) { jsonEndPos = nodeConfigString.lastIndexOf("}"); nodeConfigString = nodeConfigString.substring(0, jsonEndPos + 1); } nodeConfig = sol.common.ConfigMixin.mergeConfiguration(nodeConfigString); nodeConfig.objId = EM_ACT_SORD.id; nodeConfig.flowId = EM_WF_NODE.flowId; nodeConfig.nodeId = EM_WF_NODE.nodeId; } else { me.logger.info(["Node configuration is empty: nodeName={0}, flowName={1}, flowId={2}", EM_WF_NODE.nodeName, EM_WF_NODE.flowName, EM_WF_NODE.flowId]); me.logger.debug(["wfNode.properties={0}", properties]); me.logger.debug(["wfNode.comment={0}", comment]); } } catch (ex) { me.logger.info(["Can't parse node configuration: nodeName={0}, flowName={1}, flowId={2} exception={3})", EM_WF_NODE.nodeName, EM_WF_NODE.flowName, EM_WF_NODE.flowId, ex]); me.logger.debug(["wfNode.properties={0}", properties]); me.logger.debug(["wfNode.comment={0}", comment]); } return nodeConfig; }, /** * @private * Handles the workflow forwarding. * @param {Object} resultObj The result of either the controller function or the direct rule * @param {Object} nodeConfig The configuration of the current node */ dispatch: function (resultObj, nodeConfig) { var me = this, nextNodes; if (resultObj.passOn === true) { nextNodes = nodeConfig.nextNode || "0"; } else if (resultObj.passOn && (resultObj.passOn.all === true)) { nextNodes = me.getNextNodes(); } else if (resultObj.passOn && resultObj.passOn.nodes) { nextNodes = me.getNextNodes({ nodes: resultObj.passOn.nodes }); } if (nextNodes) { EM_WF_NEXT = nextNodes; } }, /** * @private * Writes changes to the workflow. Currently only `delayDateIso` is supported. * @param {Object} resultObj The result of either the controller function or the direct rule * @param {String} resultObj.delayDateIso The date till wich the workflow should be delayed. * @param {Object} nodeConfig The configuration of the current node */ writeWfChanges: function (resultObj, nodeConfig) { var wf, idx, currentNode; if (resultObj && resultObj.delayDateIso && nodeConfig.useDelay) { wf = ixConnect.ix().checkoutWorkFlow(String(EM_WF_NODE.flowId), WFTypeC.ACTIVE, WFDiagramC.mbAll, LockC.NO); for (idx = 0; idx < wf.nodes.length; idx++) { currentNode = wf.nodes[idx]; if (EM_WF_NODE.nodeId === currentNode.id) { currentNode.userDelayDateIso = resultObj.delayDateIso; } } ixConnect.ix().checkinWorkFlow(wf, WFDiagramC.mbAll, LockC.NO); } }, /** * @private * Determines the follow up nodes. * @param {Object} params (optional) * @param {String[]} params.nodes (optional) If set, it will be used to determine the following node IDs. This array could contain node names, translation keys or IDs. * If the parameter is not set, all successor nodes will be returned. * @return {String} Comma separated list of node IDs */ getNextNodes: function (params) { var me = this, wfEditNode, successorNodes, idx, successorNodeIndexes, currentSuccNode, i, nodeFilter, successorNodeIndexesString; wfEditNode = ixConnect.ix().beginForwardWorkflowNode(EM_WF_NODE.flowId, EM_WF_NODE.nodeId, new BeginForwardWorkflowNodeInfo(), LockC.NO); successorNodes = wfEditNode.succNodes; if (successorNodes && (successorNodes.length > 0)) { successorNodeIndexes = []; for (idx = 0; idx < successorNodes.length; idx++) { if (!params || !params.nodes || (params.nodes.length <= 0)) { successorNodeIndexes.push(idx); } else { // filter for specific nodes currentSuccNode = successorNodes[idx]; for (i = 0; i < params.nodes.length; i++) { nodeFilter = params.nodes[i]; if (nodeFilter == currentSuccNode.nameTranslationKey || nodeFilter == currentSuccNode.name || nodeFilter == currentSuccNode.id) { successorNodeIndexes.push(idx); } } } } } if (successorNodeIndexes && (successorNodeIndexes.length > 0)) { successorNodeIndexesString = successorNodeIndexes.join(sol.common.RepoUtils.pilcrow); } else { me.logger.warn("No successor nodes found. Forward to first node."); successorNodeIndexesString = "0"; } return successorNodeIndexesString; }, /** * @private * Handle an exception * @param {String|Exception} ex */ handleException: function (ex) { var me = this, errorUser = "", nodeConfig, errorMessage, wfDiagram, wfNode; nodeConfig = me.getNodeConfig() || {}; errorMessage = sol.common.ExceptionUtils.parseException(ex); if (nodeConfig.debug) { me.logger.debug(["Debug mode - don't change user"]); } else if (nodeConfig.errorNode) { EM_WF_NEXT = nodeConfig.errorNode; } else { errorUser = nodeConfig.errorUser || "0"; } if (errorUser) { wfDiagram = sol.common.WfUtils.getWorkflow(EM_WF_NODE.flowId); wfNode = sol.common.WfUtils.getNodeById(wfDiagram, EM_WF_NODE.nodeId); sol.common.WfUtils.changeNodeUser(wfNode, errorUser); ixConnect.ix().checkinWorkFlow(wfDiagram, WFDiagramC.mbAll, LockC.NO); } me.logger.warn(["Error: " + errorMessage + ": objId={0}, flowName={1}, flowId={2}, nodeName={3}, directRule={4}, nodeConfig={5}, errorUser={6}", EM_ACT_SORD.id, EM_WF_NODE.flowName, EM_WF_NODE.flowId, EM_WF_NODE.nodeName, nodeConfig.$directRule || "", JSON.stringify(nodeConfig), errorUser]); }, /** * Wait * * Node configuration examples: * * { * "$controllerFunction": "wait", * "fieldName": "INVOICE_DATACOLLECTION", * "fieldValue": "DocXtractor" * } * * { * "$controllerFunction": "wait", * "fieldName": "INVOICE_STATUS", * "fieldValueStartsWith": "7" * } * * Only forwards an entry when the date defined in GRP-field "FORWARDING_DATE" is reached. * * { * "$controllerFunction": "wait", * "fieldName": "FORWARDING_DATE", * "waitUntil": true * } * * Instead of a boolean value, `waitUntil` also accepts an integer. This integer is an offset in hours from the date which was * read from the GRP field. (days start at 00:00) * * The default forwarding behavior, in case of more then one successor node, is to forward to the first one. * To forward to all successor nodes, an additionall parameter `forwardToAll` has to be defined * * { * "$controllerFunction": "wait", * "fieldName": "INVOICE_DATACOLLECTION", * "fieldValue": "DocXtractor", * "forwardToAll": true * } * * Instead of forwarding to the first or all nodes there could be a configuration to forward to specific nodes depending on a field value. * The `forwardToNodes` parameter has an array with node configurations * * { * "$controllerFunction": "wait", * "fieldName": "INVOICE_STATUS", * "forwardToNodes": [ * { "node": "Error", "fieldValue": "DECLINED" }, * { "node": "Exported", "fieldValueStartsWith": "7" }, * { "node": "ExportedConditionally", "fieldValueStartsWith": "8" } * ] * } * * This configuration shows the following cases: * * - value of the field 'INVOICE_STATUS' is 'DECLINED' the workflow gets forwarded to the successor node 'Error' => `{ passOn: { nodes: ["Error"] } }` * - value of the field 'INVOICE_STATUS' starts with '7' the workflow gets forwarded to the successor node 'Exported' => `{ passOn: { nodes: ["Exported"] } }` * - value of the field 'INVOICE_STATUS' starts with '8' the workflow gets forwarded to the successor node 'ExportedConditionally' => `{ passOn: { nodes: ["ExportedConditionally"] } }` * * The property 'node' could either be the translation key, the name or the id of the successor node. * * The `wait` function supports the delay of workflows. This can be used to take some load of the `WfController`, if there are a lot of active workflows in the system. * There are several ways to delay a workflow. * * If `waitUntil` is used the workflow can be delayed to the specified date. * *Use carefully: if the date field changes to an earlier date, the workflow stays suspended till the former specified date.* * * { * "$controllerFunction": "wait", * "fieldName": "FORWARDING_DATE", * "waitUntil": true, * "useDelay": true * } * * This will delay the workflow till the date specified in FORWARDING_DATE. * * `useDelay` can also be used to specify a new check intervall. If specified, the WfController will check this specific workflow in longer intervalls as usual. * *Use carefully: if you specify a relative short intervall (like e.g. 1 minute) the overhead of delaying the workflow over and over again will be greater then the time the actual check would take.* * * { * "$controllerFunction": "wait", * "fieldName": "INVOICE_DATACOLLECTION", * "fieldValue": "DocXtractor", * "useDelay": true, * "intervall": { "hours": 1 } * } * * This will delay each next check for one hour. * For syntax of `intervall` see [moment.js/add (object literal)](https://momentjs.com/docs/#/manipulating/add/). * * @param {Object} nodeConfig Node configuration * @return {Object} result */ wait: function (nodeConfig) { var me = this, forwardingCfg = { passOn: false }, value, succNodes; if (nodeConfig.fieldName) { value = sol.common.SordUtils.getObjKeyValue(EM_ACT_SORD, nodeConfig.fieldName); if (nodeConfig && (nodeConfig.forwardToAll === true) && me.checkCondition(value, nodeConfig)) { // forward to all successor nodes forwardingCfg.passOn = { all: true }; } else if (nodeConfig && nodeConfig.forwardToNodes) { // forward to specific successor nodes succNodes = me.determinePassOnNodes(value, nodeConfig); forwardingCfg.passOn = (succNodes && (succNodes.length > 0)) ? { nodes: succNodes } : false; } else if (me.checkCondition(value, nodeConfig)) { // forward to first successor node forwardingCfg.passOn = true; } } // only set possible delay the workflow if passOn is not true yet if (!forwardingCfg.passOn && nodeConfig.useDelay) { if (nodeConfig.waitUntil) { forwardingCfg.delayDateIso = me.waitUntil(value, nodeConfig.waitUntil).waitUntilIso; } else if (nodeConfig.intervall) { forwardingCfg.delayDateIso = sol.common.DateUtils.dateToIso(moment().add(nodeConfig.intervall)); } } return forwardingCfg; }, /** * @private * @param {String} value Value to check * @param {Object} nodeConfig Node configuration * @return {String []} */ determinePassOnNodes: function (value, nodeConfig) { var me = this, succNodes = []; if (nodeConfig && nodeConfig.forwardToNodes && (nodeConfig.forwardToNodes.length > 0)) { nodeConfig.forwardToNodes.forEach(function (nodeCondition) { if (me.checkCondition(value, nodeCondition)) { succNodes.push(nodeCondition.node); } }); } return succNodes; }, /** * Forward * * Immediately forwards a workflow. Best used to transfer service user rights to the following node. * * Node configuration example: * * { * "$controllerFunction": "forward" * } * * @return {Object} result */ forward: function () { return { passOn: true }; }, waitUntil: function (value, waitDef) { var targetDate = sol.common.DateUtils.isoToDate(sol.common.DateUtils.dateToIso(sol.common.DateUtils.isoToDate(value), { startOfDay: true })), hours, waitUntilIso, nowIso, waitingTimeExceeded; if (sol.common.ObjectUtils.type((hours = +(waitDef)), "number")) { waitUntilIso = sol.common.DateUtils.dateToIso(moment(targetDate).add(hours, "hours")); nowIso = sol.common.DateUtils.nowIso(); } else { waitUntilIso = sol.common.DateUtils.dateToIso(targetDate); nowIso = sol.common.DateUtils.nowIso({ startOfDay: true }); } waitingTimeExceeded = (waitUntilIso <= nowIso); return { waitingTimeExceeded: waitingTimeExceeded, waitUntilIso: waitUntilIso }; }, /** * @private * @param {String} value Value to check * @param {Object} nodeConfig Node configuration * @return {Boolean} */ checkCondition: function (value, nodeConfig) { var me = this; if (nodeConfig) { if (nodeConfig.fieldValue && (nodeConfig.fieldValue == value)) { return true; } if (nodeConfig.fieldValueStartsWith && value && (value.indexOf(nodeConfig.fieldValueStartsWith) == 0)) { return true; } if (nodeConfig.waitUntil && value) { return me.waitUntil(value, nodeConfig.waitUntil).waitingTimeExceeded; } } return false; } }); var EM_WF_WITH_GROUP = true; // eslint-disable-line no-redeclare /** * Patch for the ELOas standard library function ´doWorkflow´ to include tasks for groups */ bt.doWorkflow = function () { var idx = 0, result, fti, timeZone; timeZone = ixConnect.loginResult.clientInfo.timeZone + ""; if (!timeZone) { timeZone = java.time.ZoneId.systemDefault().toString() + ""; ixConnect.loginResult.clientInfo.timeZone = timeZone; } log.info("WfController: doWorkflow(): timeZone=" + ixConnect.loginResult.clientInfo.timeZone + ", callId=" + ixConnect.loginResult.clientInfo.callId); try { fti = new FindTasksInfo(); fti.inclWorkflows = true; fti.lowestPriority = 2; fti.highestPriority = 0; fti.sordZ = (typeof EM_WF_SELECTOR !== "undefined") ? EM_WF_SELECTOR : null; fti.inclDeleted = (typeof EM_WF_WITH_DELETED !== "undefined") ? EM_WF_WITH_DELETED : false; fti.inclGroup = (typeof EM_WF_WITH_GROUP !== "undefined") ? EM_WF_WITH_GROUP : false; result = ixConnect.ix().findFirstTasks(fti, EM_SEARCHCOUNT); for (;;) { if (ruleset.getStop && ruleset.getStop()) { log.info("doWorkflow interrupted"); return; } EM_TASKLIST = result.tasks; try { bt.processTaskList(); } catch (ex2) { log.error("Error processing task list: " + ex2); } if (!result.moreResults) { break; } idx += EM_TASKLIST.length; result = ixConnect.ix().findNextTasks(result.searchId, idx, EM_SEARCHCOUNT); } } catch (ex) { log.error("Error collection task list: " + ex); return; } finally { if (result) { ixConnect.ix().findClose(result.searchId); } } };