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);
    }
  }
};