//@include lib_Class.js

/**
 * Contains workflow related utility functions
 *
 * @author PZ, ELO Digital Office GmbH
 * @version 1.04.000
 *
 * @eloix
 * @eloas
 * @elojc
 * @requires sol.common.Config
 * @requires sol.common.ConfigMixin
 * @requires sol.common.ObjectUtils
 * @requires sol.common.StringUtils
 * @requires sol.common.SordUtils
 * @requires sol.common.UserUtils
 * @requires sol.common.UserProfile
 * @requires sol.common.DateUtils
 *
 */
sol.define("sol.common.WfUtils", {
  singleton: true,

  /**
   * @private
   */
  logger: sol.create("sol.Logger", { scope: "sol.common.WfUtils" }),

  /**
   * @private
   * Loads the base configuration from the JSON file: `/Administration/Business Solutions/common/Configuration/base.config`
   * @return {Object}
   */
  loadBaseConfig: function () {
    var me = this;
    if (!me.baseConfig) {
      me.baseConfig = sol.create("sol.common.Config", { load: "ARCPATH[(E10E1000-E100-E100-E100-E10E10E10E00)]:/Business Solutions/common/Configuration/base.config" }).config;
    }
    return me.baseConfig;
  },

  /**
   * Starts a workflow.
   * @param {String} templFlowId Name or ID of the template which should be started
   * @param {String} flowName Name of the new workflow
   * @param {String} objId Object on which the workflow should be started
   * @param {Number} prio (optional) If specified, the workflow priority will be changed (0=high, 1=medium, 2=low)
   * @return {Number} The ID of the started workflow
   */
  startWorkflow: function (templFlowId, flowName, objId, prio) {
    var me = this,
        flowId;

    me.logger.enter("startWorkflow", arguments);

    if (!templFlowId) {
      throw "Workflow template ID is empty";
    }

    if (flowName && flowName.length > 120) {
      flowName = flowName.substr(0, 120);
    }

    flowId = ixConnect.ix().startWorkFlow(templFlowId, flowName, objId);
    if (typeof prio !== "undefined") {
      me.changeWorkflowPriority(flowId, prio);
    }

    me.logger.exit("startWorkflow", { flowId: flowId });

    return flowId;
  },

  /**
   * Starts the workflow defined as standard workflow for the mask or in an index field.
   * If there is already an workflow, it will do nothing and return the first active one.
   * @param {String} objId
   * @param {Object} params (optional) Default will be the sord name
   * @param {Object} params.name (optional) Default will be the sord name
   * @param {Object} params.field (optional) The field to read the workflow template from
   * @return {String} The workflow ID
   */
  startMaskStandardWorkflow: function (objId, params) {
    var me = this,
        activeWorkflows, sord, templateName, templateId, maskName, docMask, name, flowId;

    me.logger.enter("startMaskStandardWorkflow", arguments);

    activeWorkflows = sol.common.WfUtils.getActiveWorkflows(objId);

    if (activeWorkflows.length > 0) {
      return activeWorkflows[0].id;
    }

    sord = ixConnect.ix().checkoutSord(objId, SordC.mbAllIndex, LockC.NO);
    if (params && params.field) {
      templateName = sol.common.SordUtils.getObjKeyValue(sord, params.field);
      if (templateName) {
        templateId = sol.common.WfUtils.getWorkflowTemplateId(templateName);
      }
    }

    if (!templateId) {
      maskName = sord.maskName;
      docMask = sol.common.SordUtils.getDocMask(maskName);
      templateId = docMask.flowId;
    }

    if (!templateId || templateId === -1) {
      throw "mask '" + maskName + "' does not define a standard workflow";
    }

    name = (params && params.name) ? params.name : sord.name;
    if (name && name.length > 120) {
      name = name.substr(0, 120);
    }

    flowId = me.startWorkflow(templateId, name, objId);

    me.logger.exit("startMaskStandardWorkflow", { flowId: flowId });

    return flowId;
  },

  /**
   * Returns a specific workflow.
   * @param {String} flowId Flow ID
   * @param {Object} params (optional)
   * @param {Boolean} params.inclFinished (optional) If `true`, the workflows will be returned, even if it is already finished
   * @return {de.elo.ix.client.WFDiagram}
   */
  getWorkflow: function (flowId, params) {
    var workflow;
    try {
      workflow = ixConnect.ix().checkoutWorkFlow(flowId, WFTypeC.ACTIVE, WFDiagramC.mbAll, LockC.NO);
    } catch (ex) {
      if (params && (params.inclFinished === true)) {
        workflow = ixConnect.ix().checkoutWorkFlow(flowId, WFTypeC.FINISHED, WFDiagramC.mbAll, LockC.NO);
      } else {
        throw ex;
      }
    }
    return workflow;
  },

  wfDiagramTypeId: 1663767661,
  wfVersionTypeId: 914434915,
  wfNodeTypeId: 2015686193,
  wfEscalationTypeId: 633363356,
  wfEscalationUserTypeId: 123879203,

  /**
   * Returns workflow diagram as JSON string
   * @param {String} flowId Workflow ID
   * @param {String} config Configuration
   * @param {Boolean} [config.clearUsers=false] Clear the owner names of the template
   * @param {Boolean} [config.addSubTemplateInfo=false] Add sub template information
   * @return {String} JSON representation of the workflow diagram
   */
  getWorkflowAsJson: function (flowId, config) {
    var me = this,
        fileData, workflowExportOptions, workflowTplJson, workflowTplObj;

    flowId += "";

    config = config || {};

    if (!flowId) {
      throw new Error("Flow ID is missing.");
    }

    if (config.addSubTemplateInfo) {
      me.addSubTemplateInfo(flowId);
    }

    workflowExportOptions = new WorkflowExportOptions();
    workflowExportOptions.flowId = flowId;

    workflowExportOptions.format = WorkflowExportOptionsC.FORMAT_JSON;
    workflowExportOptions.flowVersId = "0";

    fileData = ixConnect.ix().exportWorkflow(workflowExportOptions);
    workflowTplJson = String(new java.lang.String(fileData.data, "UTF-8"));

    workflowTplObj = JSON.parse(workflowTplJson);

    sol.common.ObjectUtils.traverse(workflowTplObj, function (key, obj) {
      if (config.clearUsers && (obj._typeId == me.wfVersionTypeId)) {
        obj.userId = 0;
        obj.userName = "";
      }
    });

    workflowTplJson = JSON.stringify(workflowTplObj);

    return workflowTplJson;
  },

  /**
   * Adds sub template info
   * @param {Integer} flowId Flow ID
   */
  addSubTemplateInfo: function (flowId) {
    var me = this,
        propertiesObj = {},
        wfDiag, i, node;

    flowId += "";

    if (!flowId) {
      return;
    }

    wfDiag = ixConnect.ix().checkoutWorkflowTemplate(flowId, "", WFDiagramC.mbAll, LockC.NO);

    for (i = 0; i < wfDiag.nodes.length; i++) {
      node = wfDiag.nodes[i];

      if (node.type != WFNodeC.TYPE_CALL_SUB_WORKFLOW) {
        continue;
      }

      try {
        propertiesObj = JSON.parse(node.properties);
      } catch (ignore) {
        // ignore
      }
      propertiesObj.subTemplateName = me.getWorkflowTemplateName(node.subTemplateId);

      node.properties = JSON.stringify(propertiesObj, "", 2);

      me.addSubTemplateInfo(node.subTemplateId); // sub template info should also be set recursively
    }

    ixConnect.ix().checkinWorkflowTemplate(wfDiag, WFDiagramC.mbAll, LockC.NO);
  },

  /**
   * Returns the workflow name
   * @param {String} workflowJson Workflow JSON
   * @return {String} Workflow name
   */
  getWfNameFromJson: function (workflowJson) {
    var me = this,
        workflowObj, objectTable, i, obj, typeId;

    if (!workflowJson) {
      throw "Workflow JSON content is empty";
    }
    workflowObj = JSON.parse(workflowJson);
    objectTable = workflowObj.objectTable;
    for (i = 0; i < objectTable.length; i++) {
      obj = objectTable[i];
      if (obj) {
        typeId = obj["_typeId"];
        if (typeId && (typeId == me.wfDiagramTypeId)) {
          return obj.name;
        }
      }
    }
  },

  /**
   * Returns the workflow names
   * @param {String} workflowJson Workflow JSON
   * @return {Array} Workflow names
   */
  getAllWorkflowNamesFromJson: function (workflowJson) {
    var me = this,
        workflowNames = [], workflowTplObj;

    if (!workflowJson) {
      throw "Workflow JSON content is empty";
    }

    workflowTplObj = JSON.parse(workflowJson);

    sol.common.ObjectUtils.traverse(workflowTplObj, function (key, obj) {
      if (obj._typeId == me.wfDiagramTypeId) {
        workflowNames.push(obj.name);
      }
    });

    return workflowNames;
  },

  /**
   * Retrieves the active workflows for an object.
   * @param {String} objId
   * @param {Object} filter (optional)
   * @param {String} filter.template (optional) Filter results by name or ID of the workflow template
   * @param {Boolean} filter.user (optional) If `true` the active workflows will be filtered by the current user
   * @return {de.elo.ix.client.WFDiagram[]}
   */
  getActiveWorkflows: function (objId, filter) {
    var findInfo = new FindWorkflowInfo();

    findInfo.objId = objId;
    findInfo.type = WFTypeC.ACTIVE;

    if (filter && filter.template) {
      findInfo.templateId = filter.template;
    }

    return this.findWorkflows(findInfo, null, ((filter && filter.user === true) || (typeof ixConnectAdmin === "undefined")) ? ixConnect : ixConnectAdmin);
  },

  /**
   * Checks, if an object has at least on active workflow.
   * If a template name is defined, the function checks, if there is at least one active workflow with this template.
   * @param {String} objId
   * @param {String} template (optional)
   * @return {Boolean}
   */
  hasActiveWorkflow: function (objId, template) {
    var filter;
    if (template) {
      filter = { template: template };
    }
    return (this.getActiveWorkflows(objId, filter).length > 0);
  },


  /**
   * Returns the last active workflow for an object
   * @param {String} objId Object ID
   * @return {de.elo.ix.client.WFDiagram}
   */
  getLastActiveWorkflow: function (objId) {
    var me = this,
        workflows;
    if (!objId) {
      throw "Object ID is empty";
    }
    workflows = me.getActiveWorkflows(objId);
    if (workflows && (workflows.length > 0)) {
      return workflows[workflows.length - 1];
    }
  },

  /**
   * Retrieves workflows with a search request.
   * @param {de.elo.ix.client.FindWorkflowInfo} findWorkflowInfo Defines the search
   * @param {de.elo.ix.client.WFDiagramC} [checkoutOptions=WFDiagramC.mbLean] (optional) Defines the members which will be returned in the result
   * @param {de.elo.ix.client.IXConnection} [ixConn=ixConnect] (optional)
   * @return {de.elo.ix.client.WFDiagram[]}
   */
  findWorkflows: function (findWorkflowInfo, checkoutOptions, ixConn) {
    var me = this,
        max = 100,
        idx = 0,
        workflows = [],
        findResult, i, wf;

    ixConn = ixConn || ixConnect;
    checkoutOptions = checkoutOptions || WFDiagramC.mbLean;

    try {
      findResult = ixConn.ix().findFirstWorkflows(findWorkflowInfo, max, checkoutOptions);
      while (true) {
        for (i = 0; i < findResult.workflows.length; i++) {
          wf = findResult.workflows[i];
          workflows.push(wf);
        }

        if (!findResult.moreResults) {
          break;
        }
        idx += findResult.workflows.length;
        findResult = ixConn.ix().findNextWorkflows(findResult.searchId, idx, max);
      }
    } catch (ex) {
      me.logger.warn("error looking up workflows", ex);
    } finally {
      if (findResult) {
        ixConn.ix().findClose(findResult.searchId);
      }
    }
    return workflows;
  },

  /*
   * Retrieves tasks for an object.
   * @param {Object} filter (optional)
   * @param {Boolean} filter.allUsers If `true` all tasks will be returned, not just the current users tasks
   * @param {Boolean} filter.group If `true` group tasks will be included
   * @param {Boolean} filter.deputy If `true` deputy tasks will be included
   * @param {Boolean} filter.workflows If `true` workflow tasks will be included
   * @param {String} filter.objId If set, only tasks for this object will be returned
   * @return {String[]} Array of the task IDs
   *
  getTasks: function (filter) {
    var ixConn = (filter && filter.allUsers === true) ? ixConnectAdmin : ixConnect,
        findInfo = new FindTasksInfo(),
        ids = [],
        idx, max, findResult, i, wfNode;

    findInfo.inclWorkflows = (filter && filter.workflows === true);
    findInfo.inclGroup = (filter && filter.group === true);
    findInfo.inclDeputy = (filter && filter.deputy === true);
    findInfo.lowestPriority = UserTaskPriorityC.LOWEST;
    findInfo.highestPriority = UserTaskPriorityC.HIGHEST;
    if (filter && filter.objId) {
      findInfo.objId = filter.objId;
    }

    try {
      idx = 0;
      max = 100;
      findResult = ixConn.ix().findFirstTasks(findInfo, max);

      while (true) {
        for (i = 0; i < findResult.tasks.length; i++) {
          wfNode = findResult.tasks[i].wfNode;
          ids.push(wfNode.flowId);
        }
        if (!findResult.moreResults) {
          break;
        }
        idx += findResult.tasks.length;
        findResult = ixConn.ix().findNextTasks(findResult.searchId, idx, max);
      }
    } finally {
      if (findResult !== null) {
        ixConn.ix().findClose(findResult.searchId);
      }
    }
    return ids;
  },*/

  /**
   * Changes the priority of a workflow.
   * @param {String} flowId The workflows ID
   * @param {Number} prio The new priority (0=high, 1=medium, 2=low)
   */
  changeWorkflowPriority: function (flowId, prio) {
    var wfDiagram;
    if ((typeof prio !== "undefined") && (Object.prototype.toString.call(prio) === "[object Number]") && (prio >= 0) && (prio <= 2)) {
      wfDiagram = ixConnect.ix().checkoutWorkFlow(flowId, WFTypeC.ACTIVE, WFDiagramC.mbAll, LockC.NO);
      if (wfDiagram.prio !== prio) {
        wfDiagram.prio = prio;
        ixConnect.ix().checkinWorkFlow(wfDiagram, WFDiagramC.mbAll, LockC.NO);
      }
    }
  },

  /**
   * Cancels a workflow.
   * @param {String} flowId
   * @param {Boolean} force (optional) If `true`, the workflow will be terminated ignoring existing locks
   * @param {Object} params (optional) Parameters
   * @param {de.elo.ix.client.IXConnection} [params.connection=ixConnect] (optional) Index Server connection
   * @return {Number} The canceled workflow ID
   */
  cancelWorkflow: function (flowId, force, params) {
    var conn, canceledFlowId;

    params = params || {};
    conn = params.connection || ixConnect;

    canceledFlowId = conn.ix().terminateWorkFlow(flowId, (force) ? LockC.FORCE : LockC.YES);

    return canceledFlowId;
  },

  /**
   * Returns the Status of the workflow.
   * @param {String|de.elo.ix.client.WFDiagram} flow Either a flowId or WFDiagram (to avoid loading it again)
   * @return {String}
   */
  getWorkflowStatus: function (flow) {
    var me = this,
        startNode;

    if (!flow) {
      throw "Flow is empty";
    }

    if (!flow.id) {
      flow = me.getWorkflow(flow);
    }
    startNode = me.getNode(flow, 0);

    return (startNode) ? String(startNode.yesNoCondition) : "";
  },

  /**
   * Sets the workflow status.
   * @param {de.elo.ix.client.WFDiagram} wfDiagram Workflow diagram
   * @param {String} status Workflow status
   */
  setWorkflowStatus: function (wfDiagram, status) {
    var me = this,
        startNode;
    if (!wfDiagram) {
      throw "Workflow diagram is empty";
    }
    status = status || "";
    startNode = me.getNode(wfDiagram, 0);
    startNode.yesNoCondition = status;
  },

  /**
   * Retrieves the available workflow templates.
   * @param {Object} params Parameters
   * @param {de.elo.ix.client.WFDiagramC} [params.wfDiagramZ=WFDiagramC.mbLean] (optional) Defines the members which will be returned in the result
   * @return {de.elo.ix.client.WFDiagram[]}
   */
  getTemplates: function (params) {
    var me = this, ixConn, result, originalProperty,
        info;
    params = params || {};
    info = new FindWorkflowInfo();
    info.type = WFTypeC.TEMPLATE;

    //  we need to change the connection to avoid translated workflow templates
    ixConn = (typeof ixConnectAdmin === "undefined") ? ixConnect : ixConnectAdmin;
    originalProperty = ixConn.getSessionOptions().getProperty(SessionOptionsC.TRANSLATE_TERMS);
    ixConn.getSessionOptions().setProperty(SessionOptionsC.TRANSLATE_TERMS, "false");
    ixConn.getSessionOptions().update();

    result = me.findWorkflows(info, params.wfDiagramZ, ixConn);

    // and now we'll change the option back to avoid untranslated terms in all the other requests
    ixConn.getSessionOptions().setProperty(SessionOptionsC.TRANSLATE_TERMS, String(originalProperty));
    ixConn.getSessionOptions().update();

    return result;
  },

  /**
   * Returns the template ID of a workflow
   * @param {String} workflowTemplateName Name of the workflow template
   * @return {String} Workflow ID
   */
  getWorkflowTemplateId: function (workflowTemplateName) {
    var wfDiag = ixConnect.ix().checkoutWorkflowTemplate(workflowTemplateName, "", new WFDiagramZ(WFDiagramC.mbId), LockC.NO);
    return wfDiag.id;
  },

  /**
   * Returns the template name of a workflow
   * @param {String} workflowTemplateId ID of the workflow template
   * @return {String} Workflow name
   */
  getWorkflowTemplateName: function (workflowTemplateId) {
    var wfDiag, wfName;
    if (typeof workflowTemplateId == "undefined") {
      return "";
    }

    workflowTemplateId += "";

    wfDiag = ixConnect.ix().checkoutWorkflowTemplate(workflowTemplateId, "", new WFDiagramZ(WFDiagramC.mbName), LockC.NO);
    if (!wfDiag) {
      return "";
    }
    wfName = wfDiag.name + "";
    return wfName;
  },

  /**
   * Exports a workflow template into a specified file
   * @param {String} workflowTemplateName Name of the workflow template
   * @param {java.io.File} file Export file
   * @param {Object} config Configuration
   * @param {Boolean} clearAdminName Clear the administrator user name
   * @return {Array} Exported workflow template names
   */
  exportWorkflowTemplate: function (workflowTemplateName, file, config) {
    var me = this,
        workflowTemplateId, exportedWorkflowTemplateNames;

    workflowTemplateId = me.getWorkflowTemplateId(workflowTemplateName);
    exportedWorkflowTemplateNames = me.exportWorkflow(workflowTemplateId, file, config);

    return exportedWorkflowTemplateNames;
  },

  /**
   * Exports a workflow into a specified file
   * @param {String} workflowId Workflow ID
   * @param {java.io.File} file Export file
   * @param {Object} config Configuration
   * @param {Boolean} clearAdminName Clear the administrator user name
   * @return {workflowNames} Exported workflow names
   */
  exportWorkflow: function (workflowId, file, config) {
    var me = this,
        workflowJson, exportedWorkflowNames;

    workflowJson = me.getWorkflowAsJson(workflowId, config);
    Packages.org.apache.commons.io.FileUtils.writeStringToFile(file, workflowJson, "UTF-8");

    exportedWorkflowNames = me.getAllWorkflowNamesFromJson(workflowJson);

    return exportedWorkflowNames;
  },

  /**
   * Imports a workflow into a specified file
   * @param {String} workflowName Workflow name
   * @param {java.io.File} file Import file
   * @param {Object} params Parameters
   * @param {Boolean} [params.replaceMissingUserByUserId=0] Replace a missing user by this user ID
   * @return {Object} importResult Import result
   * @return {Array} importResult.replacedSubTemplateIds Replaced sub template IDs
   * @return {Integer} importResult.flowId Workflow ID
   */
  importWorkflow: function (workflowName, file, params) {
    var importResult = {},
        workflowData, workflowImportOptions, workflowTemplateJson;

    if (!workflowName) {
      throw "Workflow name is empty";
    }

    if (!file) {
      throw "Workflow file is empty";
    }

    params = params || {};
    params.replaceMissingUserByUserId = (typeof params.replaceMissingUserByUserId == "undefined") ? "0" : params.replaceMissingUserByUserId;

    workflowImportOptions = new WorkflowImportOptions();
    workflowImportOptions.replaceMissingUserByUserId = params.replaceMissingUserByUserId;

    workflowTemplateJson = Packages.org.apache.commons.io.FileUtils.readFileToString(file, "UTF-8") + "";

    workflowData = (new java.lang.String(workflowTemplateJson)).getBytes("UTF-8");

    importResult.flowId = ixConnect.ix().importWorkFlow2(workflowName, workflowData, workflowImportOptions);
    return importResult;
  },

  /**
   * Renames workflow templates
   * @param {String} oldName Old name
   * @param {String} newName New name
   * @return {Object} Result object
   */
  renameWorkflowTemplate: function (oldName, newName) {
    var me = this,
        wfDiag, result;

    if (!oldName) {
      throw "Old workflow template name is empty";
    }

    if (!newName) {
      throw "New workflow template name is empty";
    }

    try {
      wfDiag = me.getWorkflowTemplate(oldName);
    } catch (ex) {
      return;
    }

    wfDiag.name = newName;
    ixConnect.ix().checkinWorkflowTemplate(wfDiag, WFDiagramC.mbAll, LockC.NO);

    result = {
      wfTplId: wfDiag.id + "",
      oldName: oldName,
      newName: newName
    };

    return result;
  },

  /**
   * Merge workflow template
   * After the workflow templateshave been imported via JSON with a timestamp suffix, they will be stored as a new working
   * version of the origin workflow and than the imported workflow will be deleted.
   * @param {String} workflowTemplateName Workflow template name
   * @return {Object} Result object
   */
  mergeWorkflowTemplate: function (workflowTemplateName) {
    var me = this,
        i, workflowTemplates, workflowTemplate, originWorkflowTemplate, originWorkflowTemplateNamePrefix,
        currentWorkflowTemplateName, updateWorkflowTemplate, updateWorkflowTemplateId, result;

    if (!workflowTemplateName) {
      throw "Workflow template name is empty";
    }
    workflowTemplates = me.getTemplates({ wfDiagramZ: WFDiagramC.mbAll });

    for (i = 0; i < workflowTemplates.length; i++) {
      workflowTemplate = workflowTemplates[i];
      currentWorkflowTemplateName = workflowTemplate.name + "";
      originWorkflowTemplateNamePrefix = workflowTemplateName + " | origin -";

      if (currentWorkflowTemplateName.indexOf(originWorkflowTemplateNamePrefix) == 0) {
        originWorkflowTemplate = workflowTemplate;
        originWorkflowTemplate.name = workflowTemplateName;
      }

      if (currentWorkflowTemplateName == workflowTemplateName) {
        updateWorkflowTemplate = workflowTemplate;
        updateWorkflowTemplateId = updateWorkflowTemplate.id;
      }
    }

    if (!originWorkflowTemplate || !updateWorkflowTemplate) {
      return;
    }

    me.addWorkflowTemplateWorkingVersion(updateWorkflowTemplate, originWorkflowTemplate);
    me.deleteWorkflowTemplate(updateWorkflowTemplateId);

    result = {
      wfTplName: workflowTemplateName,
      wfTplId: originWorkflowTemplate.id + "",
      originWorkflow: {
        name: originWorkflowTemplate.name + "",
        wfTplId: originWorkflowTemplate.id + ""
      },
      updateWorkflow: {
        name: updateWorkflowTemplate.name + "",
        wfTplId: updateWorkflowTemplateId,
        deleted: true
      }
    };

    return result;
  },

  /**
   * @deprecated
   * @param  {de.elo.ix.client.WFDiagram} fromWorkflowTemplate Source workflow template
   * @param  {de.elo.ix.client.WFDiagram} toWorkflowTemplate Destination workflow template
   */
  addWorkflowTemplateVersions: function (fromWorkflowTemplate, toWorkflowTemplate) {
    var me = this;
    me.addWorkflowTemplateWorkingVersion(fromWorkflowTemplate, toWorkflowTemplate);
  },

  /**
   * Add the working version of a workflow template as working version of another workflow template
   * @param  {de.elo.ix.client.WFDiagram} fromWorkflowTemplate Source workflow template
   * @param  {de.elo.ix.client.WFDiagram} toWorkflowTemplate Destination workflow template
   */
  addWorkflowTemplateWorkingVersion: function (fromWorkflowTemplate, toWorkflowTemplate) {
    var me = this,
        nextWorkflowVersionNo;

    if (!fromWorkflowTemplate) {
      throw "Source workflow template is empty";
    }

    if (!toWorkflowTemplate) {
      throw "Destination workflow template is empty";
    }

    nextWorkflowVersionNo = me.getNextWorkflowVersionNo(toWorkflowTemplate);

    // save old workflow version
    toWorkflowTemplate.version.id = -1;
    ixConnect.ix().checkinWorkflowTemplate(toWorkflowTemplate, WFDiagramC.mbAll, LockC.NO);

    // set update workflow as new working version
    fromWorkflowTemplate.id = toWorkflowTemplate.id;
    fromWorkflowTemplate.version.id = 0;
    fromWorkflowTemplate.version.version = nextWorkflowVersionNo + ".0";
    ixConnect.ix().checkinWorkflowTemplate(fromWorkflowTemplate, WFDiagramC.mbAll, LockC.NO);
  },

  /**
   * Returns the highest workflow template version number
   * @param  {de.elo.ix.client.WFDiagram} workflowTemplate Workflow template
   * @return {Number} Next workflow version number
   */
  getNextWorkflowVersionNo: function (workflowTemplate) {
    var workflowTemplateVersions, i, workflowTemplateVersion, currentWorkflowTemplateVersionNo, nextWorkflowVersionNo,
        highestWorkflowTemplateVersionNo = 0;

    if (!workflowTemplate) {
      throw "Workflow template is empty";
    }

    workflowTemplateVersions = ixConnect.ix().getWorkflowTemplateVersions(workflowTemplate.id + "", false);

    for (i = 0; i < workflowTemplateVersions.length; i++) {
      workflowTemplateVersion = workflowTemplateVersions[i];
      currentWorkflowTemplateVersionNo = parseInt(workflowTemplateVersion.version, 10);
      if (currentWorkflowTemplateVersionNo > highestWorkflowTemplateVersionNo) {
        highestWorkflowTemplateVersionNo = currentWorkflowTemplateVersionNo;
      }
    }

    nextWorkflowVersionNo = highestWorkflowTemplateVersionNo + 1;

    return nextWorkflowVersionNo;
  },

  /**
   * Deletes a workflow template ID
   * @param {String} workflowTemplateId Workflow template ID
   */
  deleteWorkflowTemplate: function (workflowTemplateId) {
    if (!workflowTemplateId) {
      throw "Workflow template ID is empty";
    }
    ixConnect.ix().deleteWorkflowTemplate(workflowTemplateId + "", "", LockC.NO);
  },

  /**
   * Returns the workflow template
   * @param {String} workflowTemplateId Workflow template ID
   * @return {String} Workflow ID
   */
  getWorkflowTemplate: function (workflowTemplateId) {
    if (!workflowTemplateId) {
      throw "Workflow template ID is empty";
    }
    var wfDiag = ixConnect.ix().checkoutWorkflowTemplate(workflowTemplateId + "", "", WFDiagramC.mbAll, LockC.NO);
    return wfDiag;
  },

  /**
   * This function parses the configuration object from a nodes comment.
   * The comment has to be valid JSON string.
   *
   * Additionally this method supports parsing the configuration from a config file (see {@link sol.common.ConfigMixin#parseConfiguration}).
   *
   * If $useTemplating: true is defined in the comment, the node's comment will be templated using handlebars.
   *
   * Should the workflow's sord be needed in templating, additionally, $useTemplateSord: true can be added to the comment.
   *
   * In addition there can be a variable number of string arguments for mandatory properties. If the configuration is missing one of those properties, an exception will be thrown.
   *
   *     var config = sol.common.ix.WfUtils.parseAndCheckParams(wf, 23, "objId", ...);
   *
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} nodeId The ID of the node
   * @return {Object}
   */
  parseAndCheckParams: function (workflow, nodeId) {
    var me = this,
        emptyArray = [],
        mandatoryProperties, node, commentString, comment, config, nodeComment;

    mandatoryProperties = (arguments.length > 2) ? emptyArray.slice.call(arguments, 2) : emptyArray;
    node = me.getNode(workflow, nodeId);

    nodeComment = node.comment + "";
    nodeComment = ((nodeComment.length > 0) && (nodeComment.trim().charAt(0) == "{")) ? nodeComment : "";

    commentString = String(node.properties) || nodeComment || "{}";

    try {
      comment = JSON.parse(commentString);

      if (comment.$useTemplating) {
        commentString = me.templateMixin(commentString, (comment.$useTemplateSord && workflow.objId), workflow.id);
        comment = JSON.parse(commentString);
      }

      config = sol.common.ConfigMixin.parseConfiguration(comment, undefined, true).config;
    } catch (ex) {
      me.logger.error(["error reading node config of node '{0}': {1}", node.name, commentString], ex);
      throw "configuration syntax error (node='" + node.name + "'): " + ex + " - config=" + commentString;
    }

    mandatoryProperties.forEach(function (property) {
      if (!config.hasOwnProperty(property)) {
        throw "configuration error: missing parameter '" + property + "'";
      }
    });

    return config;
  },

  /**
   * Applies handlebars template to mixin.
   * @param {Object} mixinString The config mixin string
   * @param {String} objId The WFDiagram's objId
   * @param {String} flowId The WFDiagram's flowId
   * @return {Object} Object which resulted from templating
   */
  templateMixin: function (mixinString, objId, flowId) {
    var me = this, sord, templatingData = {};

    if (!sol.common.TemplateUtils) {
      throw "$useTemplating and $useTemplateSord can only be used with IX scripts which include lib_sol.common.Template";
    }
    if (objId) {
      try {
        sord = (typeof ixConnectAdmin !== "undefined" ? ixConnectAdmin : ixConnect).ix().checkoutSord(objId, SordC.mbAllIndex, LockC.NO);
        templatingData.sord = me.getTemplateSord(sord, flowId, { formBlobs: true, asAdmin: true }).sord;
      } catch (_) {
        throw "could not create templatesord for templating workflowMixins.";
      }
    }
    return sol.common.TemplateUtils.render(mixinString, templatingData, { emptyNonRendered: true });
  },

  /**
   * Returns the node defined by the nodeId.
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} id The ID of the node
   * @return {de.elo.ix.client.WFNode} The node or null, if no node was found with the ID
   */
  getNode: function (workflow, id) {
    var node, i;
    for (i = 0; i < workflow.nodes.length; i++) {
      node = workflow.nodes[i];
      if ((node.id == id) && (node.type != WFNodeC.TYPE_NOTHING)) {
        return node;
      }
    }
    return null;
  },

  /**
   * Returns the name of the node.
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} nodeId
   * @return {String} The name of the node or null, if no node was found with the ID
   */
  getNodeName: function (workflow, nodeId) {
    var node = this.getNode(workflow, nodeId);
    return node ? node.name : null;
  },

  /**
   * Returns all active workflow nodes.
   * @param {de.elo.ix.client.WFDiagram} workflow
   * @return {de.elo.ix.client.WFNode[]}
   */
  getActiveNodes: function (workflow) {
    var nodes = [],
        i;
    for (i = 0; i < workflow.nodes.length; i++) {
      if (workflow.nodes[i].enterDateIso != "" && workflow.nodes[i].exitDateIso == "") {
        nodes.push(workflow.nodes[i]);
      }
    }
    return nodes;
  },

  /**
   * Returns all active workflow nodes with assigned users or groups.
   * @param {de.elo.ix.client.WFDiagram} workflow
   * @return {de.elo.ix.client.WFNode[]}
   */
  getActiveUserNodes: function (workflow) {
    var nodes = [],
        i, node;
    for (i = 0; i < workflow.nodes.length; i++) {
      node = workflow.nodes[i];
      if (node.enterDateIso != "" && node.exitDateIso == "" && node.userName != "") {
        nodes.push(node);
      }
    }
    return nodes;
  },

  /**
   * Returns the node for a node name.
   * If the node name is not unique, the first found node will be returned.
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} name The name of the node
   * @param {String}  (optional) Cycle number
   * @return {de.elo.ix.client.WFNode} The node or null, if no node was found with the name
   */
  getNodeByName: function (workflow, name, cycleNo) {
    var node, i;
    if (!workflow) {
      throw "Workflow diagram is empty";
    }
    if (!name) {
      throw "Name is empty";
    }
    for (i = 0; i < workflow.nodes.length; i++) {
      node = workflow.nodes[i];
      if ((node.name == name || node.nameTranslationKey == name) && (node.type != WFNodeC.TYPE_NOTHING)) {
        if (!cycleNo || sol.common.StringUtils.endsWith(node.name, "[[" + cycleNo + "]]")) {
          return node;
        }
      }
    }
    return null;
  },

  /**
   * Returns the node for a node ID.
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} nodeId Node ID
   * @return {de.elo.ix.client.WFNode} The node or null, if no node was found with the ID
   */
  getNodeById: function (workflow, nodeId) {
    var node, i;
    for (i = 0; i < workflow.nodes.length; i++) {
      node = workflow.nodes[i];
      if ((node.id == nodeId) && (node.type != WFNodeC.TYPE_NOTHING)) {
        return node;
      }
    }
    return null;
  },

  /**
   * Retrieves a list of nodes which are successors to the node with the spezified ID.
   * The returned nodes could be filtered by type.
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} nodeId The ID of the node
   * @param {Number} filterType (optional) The number of the type (use de.elo.ix.client.WFNodeC.TYPE_* for the types)
   * @return {de.elo.ix.client.WFNode[]}
   */
  getSuccessorNodes: function (workflow, nodeId, filterType) {
    var me = this,
        assocs = workflow.matrix.assocs,
        succNodes = [];

    assocs.forEach(function (assoc) {
      var node;
      if (assoc.nodeFrom == nodeId) {
        node = me.getNode(workflow, assoc.nodeTo);
        if (!filterType || filterType == node.type) {
          succNodes.push(node);
        }
      }
    });

    return succNodes;
  },

  /**
   * Returns the successor node
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} nodeId The ID of the node
   * @param {Object} filterConfig Filte configuration
   * @param {Array} filterConfig.nameTranslationKeys Translation keys
   * @param {Array} filterConfig.iconNames Icon names
   * @return {de.elo.ix.client.WFNode}
   */
  getSuccessorNode: function (workflow, nodeId, filterConfig) {
    var me = this,
        succNodes;

    succNodes = me.getSuccessorNodes2(workflow, nodeId, filterConfig);

    if (succNodes.length > 1) {
      throw "More than one successor node found.";
    }

    if (succNodes.length == 1) {
      return succNodes[0];
    }
  },

  /**
   * Returns the successor nodes
   * @param {de.elo.ix.client.WFDiagram} workflow The WFDiagram containing the workflow description
   * @param {String} nodeId The ID of the node
   * @param {Object} filterConfig Filter configuration
   * @param {Array} filterConfig.nameTranslationKeys Translation keys
   * @param {Array} filterConfig.iconNames Icon names
   * @return {de.elo.ix.client.WFNode[]}
   */
  getSuccessorNodes2: function (workflow, nodeId, filterConfig) {
    var me = this,
        succNodes = [],
        assocs, assoc, i, j, node,
        nameTranslationKeys, nameTranslationKey,
        iconNames, iconGuid;

    assocs = workflow.matrix.assocs;

    for (i = 0; i < assocs.length; i++) {
      assoc = assocs[i];
      if (assoc.nodeFrom == nodeId) {
        node = me.getNode(workflow, assoc.nodeTo);
        if (filterConfig) {
          if (filterConfig.nameTranslationKeys) {
            nameTranslationKeys = filterConfig.nameTranslationKeys;
            for (j = 0; j < nameTranslationKeys.length; j++) {
              nameTranslationKey = nameTranslationKeys[j];
              if (node.nameTranslationKey == nameTranslationKey) {
                succNodes.push(node);
                continue;
              }
            }
          }
          if (filterConfig.iconNames) {
            iconNames = filterConfig.iconNames;
            for (j = 0; j < iconNames.length; j++) {
              iconGuid = me.getWorkflowIconGuid(iconNames[j]);
              if (iconGuid && (node.iconId == iconGuid)) {
                succNodes.push(node);
              }
            }
          }
        } else {
          succNodes.push(node);
        }
      }
    }

    return me.getUniqueNodes(succNodes);
  },

  /**
   * Returns the guid of a workflow icon
   * @param {String} iconName Icon name
   * @return {String} Workflow icon GUID
   */
  getWorkflowIconGuid: function (iconName) {
    var me = this,
        guid;
    if (!me.workflowIconGuids) {
      me.readWorkflowIconGuids();
    }
    guid = me.workflowIconGuids[iconName];
    return guid;
  },

  /**
   * Activates associations
   * @param  {de.elo.ix.client.WFNodeAssoc[]} assocs Associations
   */
  activateAssocs: function (assocs) {
    var me = this;
    if (!assocs || (assocs.length == 0)) {
      return;
    }
    assocs.forEach(function (assoc) {
      me.logger.debug(["Activate association: nodeFrom={0}, nodeTo={1}, assocType={2}", assoc.nodeFrom, assoc.nodeTo, assoc.type]);
      assoc.done = true;
    });
  },

  workflowIconFolderGuid: "(E10E1000-E100-E100-E100-E10E10E10EE0)",

  /**
   * Reads the workflow icon IDs
   * @private
   */
  readWorkflowIconGuids: function () {
    var me = this,
        wfIconGuids = {},
        sords;

    sords = sol.common.RepoUtils.findChildren(me.workflowIconFolderGuid, { includeDocuments: true, includeReferences: true, sordZ: SordC.mbMin });
    sords.forEach(function (sord) {
      wfIconGuids[sord.name] = sord.guid;
    });
    me.workflowIconGuids = wfIconGuids;
  },

  /**
   * Returns a list of unique workflow nodes
   * @param {Array} nodes Workflow nodes
   * @returns {Array} Unique workflow nodes
   */
  getUniqueNodes: function (nodes) {
    var key,
        nodesObj = {},
        filteredNodes = [];

    if (!nodes) {
      throw "Nodes array must be given";
    }
    nodes.forEach(function (node) {
      nodesObj[String(node.id)] = node;
    });
    for (key in nodesObj) {
      if (nodesObj.hasOwnProperty(key)) {
        filteredNodes.push(nodesObj[key]);
      }
    }
    return filteredNodes;
  },

  /**
   * Forward a workflow
   * @param {String} flowId Flow ID
   * @param {String} currentNodeId Current node ID
   * @param {Array} destinationNodeIds Destination node ID
   */
  forwardWorkflow: function (flowId, currentNodeId, destinationNodeIds) {
    var me = this;

    if (!flowId) {
      throw "Flow ID is empty";
    }

    if (!currentNodeId) {
      throw "Current node ID is empty";
    }

    if (!destinationNodeIds) {
      throw "Destination node IDs are empty";
    }

    ixConnect.ix().beginEditWorkFlowNode(flowId, currentNodeId, LockC.YES);
    ixConnect.ix().endEditWorkFlowNode(flowId, currentNodeId, false, false, null, null, destinationNodeIds);

    me.logger.debug(["Workflow forwarded (flowId={0}, currentNodeId={1}, destinationNodeIds={2})", flowId, currentNodeId, destinationNodeIds]);
  },

  /**
   * Changes the user of a node.
   * @param {de.elo.ix.client.WFNode} node The node to be changed
   * @param {String} user The user which should be set
   * @param {Object} params Parameters
   * @param {Boolean} [params.changeDesignDepartment=false] Change also the property `designDepartment`
   */
  changeNodeUser: function (node, user, params) {
    var me = this,
        userInfo;

    params = params || {};

    if (node) {
      if (node.userName != user) {
        me.logger.debug(["changeNodeUser: node.name={0}, node.userName={1}", node.name, user]);
        node.userName = user;
        node.userId = -1;
        if (params.changeDesignDepartment) {
          userInfo = sol.common.UserUtils.getUserInfo(user);
          if (userInfo) {
            me.logger.debug(["changeNodeUser: node.name={0}, node.designDepartment={1}, node.designDepartmentName={2}", node.name, userInfo.id, userInfo.name]);
            node.designDepartment = userInfo.id;
          }
        }
      }
    } else {
      this.logger.warn("'node' cannot be empty, no user set.");
    }
  },

  /**
   * Sets node escalations
   * @param {de.elo.ix.client.WFNode} node The node to be changed
   * @param {Object[]} nodeEscalations Node escalations
   * @param {Object} nodeEscalations.user Node escalation user
   * @param {String} nodeEscalations.user.value Node escalation user name
   * @param {Boolean} nodeEscalations.user.supervisor Escalate to the users supervisor
   * @param {Number} nodeEscalations.timeLimitMinutes Node escalation minutes
   *
   * Example:
   *     [
   *       { "timeLimitMinutes": 1, "user": { "value": "User1" } }
   *     ]
   *
   * @param {String} defaultUserName Default user name
   *
   */
  setNodeEscalations: function (node, nodeEscalations, defaultUserName) {
    var me = this,
        i, nodeEscalation, userName;

    nodeEscalations = nodeEscalations || [];

    if (!node) {
      throw "Node is empty";
    }

    if (!nodeEscalations || !nodeEscalations.length) {
      return;
    }

    for (i = 0; i < 2; i++) {
      nodeEscalation = nodeEscalations[i];
      if (!nodeEscalation) {
        continue;
      }

      userName = (nodeEscalation.user && (typeof nodeEscalation.user.value != "undefined")) ? nodeEscalation.user.value : defaultUserName;

      if (nodeEscalation.user && (nodeEscalation.user.supervisor === true)) {
        userName = sol.common.UserUtils.getSupervisor(userName);
      }

      node.timeLimitEscalations[i].userId = -1;
      node.timeLimitEscalations[i].userName = userName;

      if (typeof nodeEscalation.timeLimitMinutes != "undefined") {
        node.timeLimitEscalations[i].timeLimit = nodeEscalation.timeLimitMinutes;
      }

      me.logger.debug(["Set escalation {0}: node.name={1}, userName={2}", i + "", node.name + "", userName + ""]);
    }
  },

  /**
   * Changes the name of a node.
   * @param {de.elo.ix.client.WFNode} node The node to be changed
   * @param {String} name The new node name
   */
  changeNodeName: function (node, name) {
    if (node) {
      node.name = name;
    } else {
      this.logger.warn("'node' cannot be empty, name not changed.");
    }
  },

  /**
   * Appends a String to a nodes comment field.
   * @param {de.elo.ix.client.WFNode} node The node to be changed
   * @param {String} comment
   * @param {Boolean} override (optional) If `true`, an existing comment will be overridden
   */
  appendNodeComment: function (node, comment, override) {
    comment = comment || "";
    if (override) {
      node.comment = comment;
      return;
    }

    if (node.comment && node.comment.length() > 0) {
      node.comment += "\n";
    }
    node.comment += comment;
  },

  /**
   * Finds the first active node of a specific object and workflow
   * and returns the node ID and the URL.
   * @param {String} objId Object ID
   * @param {String} flowId Flow ID
   * @return {Object} nodeId, url. Node ID and URL
   */
  findFirstActiveNodeWithUrl: function (objId, flowId) {
    var me = this,
        wfCollectNode, url;
    wfCollectNode = me.findFirstActiveNode(objId, flowId);
    if (wfCollectNode) {
      url = me.getFormUrl(wfCollectNode);
      return { nodeId: wfCollectNode.nodeId, url: url };
    }
  },

  /**
   * Finds the first active Node of a specific object and workflow
   * @param {String} objId Object ID
   * @param {String} flowId Flow ID
   * @return {de.elo.ix.client.WFCollectNode} wfCollectNode. Found active node.
   */
  findFirstActiveNode: function (objId, flowId) {
    var me = this,
        findTasksInfo, idx, findResult, tasks, i, wfCollectNode;

    me.logger.enter("findFirstActiveNode", { objId: objId, flowId: flowId });

    findTasksInfo = new FindTasksInfo();
    findTasksInfo.inclWorkflows = true;
    findTasksInfo.lowestPriority = UserTaskPriorityC.LOWEST;
    findTasksInfo.highestPriority = UserTaskPriorityC.HIGHEST;
    findTasksInfo.objId = objId;

    idx = 0;
    findResult = ixConnect.ix().findFirstTasks(findTasksInfo, 100);

    while (true) {
      tasks = findResult.tasks;
      for (i = 0; i < tasks.length; i++) {
        wfCollectNode = tasks[i].wfNode;
        if ((wfCollectNode.flowId == flowId) || (wfCollectNode.parentFlowId == flowId)) {
          me.logger.debug(["found active node for workflow (flowId={0}, nodeName={1}, nodeId={2})", wfCollectNode.flowId, wfCollectNode.nodeName, wfCollectNode.nodeId]);
          me.logger.exit("findFirstActiveNode");
          return wfCollectNode;
        }
      }

      if (!findResult.isMoreResults()) {
        break;
      }

      idx += findResult.tasks.length;
      findResult = ixConnect.ix().findNextTasks(findResult.searchId, idx, 100);
    }

    if (findResult) {
      ixConnect.ix().findClose(findResult.searchId);
    }

    me.logger.debug(["no active node found for flowId={0}", flowId]);
    me.logger.exit("findFirstActiveNode");
    return null;
  },

  /**
   * Returns the ELOwf form URL of a specified node
   * @param {de.elo.ix.client.WFCollectNode} wfCollectNode
   * @return {String} url URL of the ELOwf form
   */
  getFormUrl: function (wfCollectNode) {
    var me = this,
        formName, baseUrl, url,
        urlParams = [];
    urlParams.push("wfid=" + wfCollectNode.flowId);
    urlParams.push("nodeid=" + wfCollectNode.nodeId);
    urlParams.push("ticket=" + ixConnect.loginResult.clientInfo.ticket);
    urlParams.push("lang=" + ixConnect.loginResult.clientInfo.language);

    formName = me.getFormName(wfCollectNode);
    baseUrl = me.getWfBaseUrl();

    if (!formName || !baseUrl) {
      return "";
    }

    url = baseUrl + "/" + formName + ".jsp?" + urlParams.join("&");
    return url;
  },

  /**
   * Returns the ELOwf base URL
   * @return {String} url. ELOwf base URL.
   */
  getWfBaseUrl: function () {
    var me = this,
        ixVersion, ixUrl, globalProfile;

    if (!me.wfBaseUrl) {
      ixVersion = ixConnect.implVersion + "";

      // For ELO versions 2x (20, 21 etc.) the proxy WF URL should be used
      if (ixVersion.substring(0, 1) == "2") {
        ixUrl = sol.common.RepoUtils.getIxOption("publicUrlBase");
        if (!ixUrl) {
          ixUrl = ixConnect.endpointUrl + "";
        }
        me.wfBaseUrl = ixUrl.substring(0, ixUrl.length - 3) + "/plugin/de.elo.ix.plugin.proxy/wf";
        return me.wfBaseUrl;
      }

      globalProfile = sol.create("sol.common.UserProfile", { userId: UserProfileC.USERID_ALL });
      me.wfBaseUrl = String(globalProfile.getOption("Client.1398.1.0.Options.EloWfUrl.")).replace(/\/$/, "");
    }

    return me.wfBaseUrl;
  },

  /**
   * Checks wether the ELOwf is running
   * @return {Object}
   * @return {Boolean} httpResponse.isRunning
   */
  checkWfIsRunning: function () {
    var me = this,
        wfStatusUrl, httpResponse;

    wfStatusUrl = me.getWfBaseUrl() + "/wf?__cmd__=status";

    httpResponse = sol.common.HttpUtils.sendRequest({ url: wfStatusUrl, resolve: false, connectTimeout: 3000, readTimeout: 10000 });

    if (httpResponse.responseOk && (httpResponse.content.indexOf(">ELOwf Status Report<") > -1) && (httpResponse.content.indexOf(">Running<") > -1)) {
      httpResponse.isRunning = true;
    } else {
      httpResponse.isRunning = false;
    }

    return httpResponse;
  },

  /**
   * Returns form name of a specified node
   * @param {de.elo.ix.client.WFCollectNode} wfCollectNode
   * @return {String} formName. Form name.
   */
  getFormName: function (wfCollectNode) {
    var me = this,
        formStartPos, formEndPos,
        formSpec = String(wfCollectNode.formSpec);
    if (!formSpec) {
      me.logger.warn("Property 'formSpec' is empty: node.name=" + wfCollectNode.name);
      return "";
    }
    formStartPos = formSpec.indexOf("[") + 1;
    formEndPos = formSpec.indexOf("(");
    return formSpec.substring(formStartPos, formEndPos);
  },

  /**
   * Creates a template workflow node from a WFDiagram (see {@link sol.common.ObjectFormatter.TemplateWfDiagramNode TemplateWfDiagramNode}).
   * @param {de.elo.ix.client.WFDiagram} wfDiagram
   * @param {Number} nodeId
   * @return {Object}
   */
  getTemplateWfDiagramNode: function (wfDiagram, nodeId) {
    if (!wfDiagram) {
      throw "wfDiagram is empty";
    }
    if (!nodeId) {
      throw "Node ID is empty";
    }

    return sol.common.ObjectFormatter.format({
      node: {
        formatter: "sol.common.ObjectFormatter.TemplateWfDiagramNode",
        data: wfDiagram,
        config: {
          flowId: wfDiagram.id,
          nodeId: nodeId
        }
      }
    });
  },

  /**
   * Retrieves a map value from a workflow.
   * @param {String} flowId Flow ID
   * @param {String} key Map key
   * @return {String} value
   */
  getWfMapValue: function (flowId, key) {
    var tmpValue,
        value = "";

    tmpValue = ixConnect.ix().checkoutMap(MapDomainC.DOMAIN_WORKFLOW_ACTIVE, flowId, [key], LockC.NO).items;
    if (tmpValue && tmpValue.length == 1) {
      value = tmpValue[0].value;
    }
    return value;
  },

  /**
   * Sets a map value from a workflow.
   * @param {Number} objId Flow ID
   * @param {String} flowId Flow ID
   * @param {String} key Map key
   * @param {String|Array} values Value
   */
  setWfMapValue: function (objId, flowId, key, values) {
    var i,
        keyValues = [];

    if (!objId) {
      throw "Object ID is empty";
    }

    if (!flowId) {
      throw "Flow ID is empty";
    }

    if (!key) {
      throw "Map key is empty";
    }
    values = values || "";

    objId = parseInt(objId, 10);

    if (sol.common.ObjectUtils.isArray(values)) {
      for (i = 1; i <= values.length; i++) {
        keyValues.push(new KeyValue(key + i, values[i - 1]));
      }
      keyValues.push(new KeyValue(key + i, ""));
    } else {
      keyValues.push(new KeyValue(key, values));
    }

    ixConnect.ix().checkinMap(MapDomainC.DOMAIN_WORKFLOW_ACTIVE, flowId, objId, keyValues, LockC.NO);
  },

  /**
   * Get node User
   * @param {de.elo.ix.client.WFDiagram} wfDiagram Workflow diagram
   * @param {String} nodeId Node ID
   * @param {Object} config Configuration
   * @param {Boolean} config.useSessionUserAlternatively
   * @return {String} Node user
   */
  getNodeUser: function (wfDiagram, nodeId, config) {
    var me = this,
        wfNode,
        nodeUser = "";

    config = config || {};

    if (wfDiagram && nodeId) {
      wfNode = me.getNode(wfDiagram, nodeId);
      nodeUser = String(wfNode.userName);
    }

    if (!nodeUser && config.useSessionUserAlternatively) {
      nodeUser = String(ixConnect.loginResult.user.name);
    }
    return nodeUser;
  },

  /**
   * Returns the cycle number
   * @param {String} nodeName Node name
   * @return {String}
   */
  getCycleNumber: function (nodeName) {
    var matches;

    matches = String(nodeName).match(/(\[\[)(\d+)(\]\]$)/);
    if (matches && (matches.length == 4)) {
      return matches[2];
    }
  },

  /**
   * Appends the cycle number to a node name
   * @param {String} nodeName Node name
   * @param {String} cycleNo Cycle number
   * @return {String}
   */
  appendCycleNumber: function (nodeName, cycleNo) {
    return cycleNo ? nodeName + " [[" + cycleNo + "]]" : nodeName;
  },

  /**
   * Retrieves the prefix for service workflows from the `serviceWfPrefix` property from `base.config` file.
   * @return {String}
   */
  getServiceWfPrefix: function () {
    var me = this;
    return me.loadBaseConfig().serviceWfPrefix;
  },

  /**
   * Checks, if the workflow template was created of a user with main admin rights.
   * @param {de.elo.ix.client.WFDiagram} wfDiagram
   */
  checkMainAdminWf: function (wfDiagram) {
    var creator;
    creator = (wfDiagram && wfDiagram.version) ? wfDiagram.version.userId : "";

    if (creator == -1) {
      throw "Workflow creator ID is not valid: userId=" + creator;
    }

    if (!sol.common.UserUtils.isMainAdmin(creator)) {
      throw "This workflow uses an admin function node. Hence this workflow template has to be created by an administrative user. It was created by '" + +(creator) + "'";
    }
  },

  /**
   * Checks, if a workflow was started from a service.
   *
   * Currently this is determined by the prefix at the workflow name retrieved by {@link #getServiceWfPrefix}.
   * @param {de.elo.ix.client.WFDiagram} wfDiagram
   * @return {Boolean}
   */
  isServiceWf: function (wfDiagram) {
    var me = this,
        prefix, wfName, isServiceWf;
    me.logger.enter("isServiceWf", arguments);
    prefix = me.getServiceWfPrefix();
    wfName = wfDiagram.name;
    isServiceWf = sol.common.StringUtils.startsWith(wfName, prefix);
    me.logger.exit("isServiceWf", { isServiceWf: isServiceWf });
    return isServiceWf;
  },

  /**
   * Creates the name for a service workflow from a given name.
   *
   * Currently this is implemented by using the value retrieved by {@link #getServiceWfPrefix} as prefix.
   * @param {String} wfName
   * @return {String}
   */
  createServiceWfName: function (wfName) {
    var me = this,
        prefix;
    me.logger.enter("createServiceWfName", arguments);
    prefix = me.getServiceWfPrefix();
    if (prefix) {
      wfName = prefix + wfName;
    }
    me.logger.exit("createServiceWfName", { wfName: wfName });
    return wfName;
  },

  /**
   * Sets the session option ´Start docmask workflows´
   * @param {Boolean} startDocMaskWorkflows Start doc mask workflows
   */
  setSessionOptionStartDocMaskWorkflows: function (startDocMaskWorkflows) {
    var sessionOptions;

    if (typeof startDocMaskWorkflows !== "boolean") {
      return;
    }
    sessionOptions = {};
    sessionOptions[SessionOptionsC.START_DOCMASK_WORKFLOWS] = startDocMaskWorkflows ? "true" : "false";

    sol.common.RepoUtils.setSessionOptions(sessionOptions);
  },

  /**
   * Returns a template sord including wfMap data
   * @param {de.elo.ix.client.Sord} sord sord for object formatter
   * @param {String} flowId Workflow id of workflow map
   * @param {Object} options Options
   * @param {Boolean} options.asAdmin As admin
   * @return {Object} Template Sord
   */
  getTemplateSord: function (sord, flowId, options) {
    var me = this,
        templateSord;

    me.logger.enter("getTemplateSord", arguments);
    templateSord = sol.common.ObjectFormatter.format({
      sord: {
        formatter: "sol.common.ObjectFormatter.WfMap",
        data: sord,
        config: {
          sordKeys: ["id", "guid", "maskName", "name", "desc", "IDateIso", "XDateIso", "ownerName"],
          allMapFields: true,
          allFormBlobFields: (options && options.formBlobs === true),
          flowId: flowId,
          asAdmin: (options && options.asAdmin) || false
        }
      }
    });
    me.logger.exit("getTemplateSord");
    return templateSord;
  },

  /**
   * Creates a new reminder.
   * @since 1.04.000
   * @param {String} objId
   * @param {Object} params (optional) Additional parameters
   * @param {String[]} params.userIds (optional) The user ids of the reminder receivers. Default is the IX connection user.
   * @param {String} params.name (optional) The name for the reminder. Default is `sord.name`.
   * @param {String} params.promptDateIso (optional) The date (as ISO string) when the reminder will be set visible for the user. Defualt is the current date.
   * @return {Number[]} The reminder ids. There will be one reminderfor each user in `params.userIds` if defined or one reminder for the current user.
   */
  createReminder: function (objId, params) {
    var me = this,
        name, promptDate, receiverIds, reminder, reminderIds;

    name = (params && params.name) ? params.name : ixConnect.ix().checkoutSord(objId, SordC.mbMin, LockC.NO).name;
    promptDate = (params && params.promptDateIso) ? params.promptDateIso : sol.common.DateUtils.dateToIso(new Date());
    receiverIds = (params && params.userIds) ? params.userIds : [ixConnect.loginResult.user.id];

    reminder = ixConnect.ix().createReminder(objId);
    reminder.name = name;
    reminder.promptDateIso = promptDate;
    reminderIds = ixConnect.ix().checkinReminder(reminder, receiverIds, false, LockC.NO);

    me.logger.info(["reminder created: objId={0}, users={1}", objId, receiverIds.join(",")]);

    return reminderIds;
  },

  /**
   * Checks if the passed node is from type WFNodeC.TYPE_CALL_SUB_WORKFLOW
   * @param {de.elo.ix.client.WFNode} node which should be checked
   */
  isSubworkflowNode: function (node) {
    return node && node.type === WFNodeC.TYPE_CALL_SUB_WORKFLOW;
  }

});