// Java includes
importPackage(Packages.de.elo.ix.client);

// JavaScript includes
//@include lib_Class.js

/**
 * Utility functions for ACL processing.
 *
 * The functions `addRights` and `removeRights` apply changes to an objects ACL items.
 * The `restore` function can restore the original rights if needed, and if there was a call to one of the before mentioned functions with a store command.
 *
 * ## Function parameters
 * For examples see {@link sol.common.AclUtils#addRights addRights} and {@link sol.common.AclUtils#removeRights removeRights}
 *
 * ### objId
 * ObjId of the Object which should be edited. If there is a `recursive` command (see config parameter) this is the starting point from which all sub-elements will ne processed.
 *
 * ### users (only `addRights` and `removeRights`)
 * This is an Array with user names or user objects (or a mix of both).
 * If there are valid usernames specified, only those users ACL items will be altered.
 * If this contains objects they need to have a `name` property and may have an additional `rights` object. E.g.:
 *
 *     {
 *       users: [
 *         { name: "AMustermann", rights: { r: true, w: true } },
 *         { name: "BMustermann", rights: { r: true, w: true, d: true } },
 *         { name: "CMustermann" },   // will get the fallback access rights
 *         "DMustermann"              // will get the fallback access rights
 *       ]
 *     }
 *
 * If this parameter is undefined or an empty Array, all existing ACL items will be adjusted.
 *
 * ### rights (only `addRights` and `removeRights`)
 * This Object specifies, which rights should be added/removed.
 * Both following forms are valid:
 *
 *     { r: true, w: true, d: false, e: false, l: false, p: false}
 *     { read: false, write: true, del: true, edit: true, list: true. perm: true}
 *
 * The `addRights` function will add all right, flaged with `true`, while `removeRights` will remove all rights flaged with `true`.
 *
 * ### config
 * This Object contains additional processing information.
 * Currently the following parameters are supported:
 *
 * - recursive: if `true`, all sub-elements will be processed
 * - storeAcl: Object which defines the store path: `{ type: "MAP", key: "OLD_ACL" }`. Currently only map fields are supported.
 * - asAdmin: if `true`, the rights will be altered in admin context.
 *
 * ## Compatibility
 * All usage of the permission right ('p' or 'perm') is only supported in ELO12 and later.
 *
 * @author PZ, ELO Digital Office GmbH
 * @version 1.06.000
 *
 * @elojc
 * @eloas
 * @eloix
 * @requires sol.common.SordUtils
 * @requires sol.common.RepoUtils
 * @requires sol.common.UserUtils
 * @requires sol.common.JsonUtils
 * @requires sol.common.ObjectFormatter
 * @requires sol.common.Template
 * @requires sol.common.AsyncUtils
 */
sol.define("sol.common.AclUtils", {
  singleton: true,

  /**
   * @private
   * @property
   */
  sordZ: SordC.mbMin,

  /**
   * Adds rights to an archive entry/entries.
   *
   * The following example grants read, write and permission rights to the users "baum" and "renz" on exactly one object
   *
   *     sol.common.AclUtils.addRights(
   *       "4711",
   *       ["baum", "renz"],
   *       { r: true, w: true, d: false, e: false, l: false, p: true},
   *       { }
   *     );
   *
   * @param {String} objId The object which should be edited (if config has a `recursive` flag set to `true`, this object will be the starting point)
   * @param {String[]|Object[]} users If this contains strings, they serve as user names. If it contains objects, they have to contain a `name` property and an `rights` object. If empty, all existing ACL entries will be edited.
   * @param {Object} rights Object with flags for each right that should be added
   * @param {Object} config (optional) Additional configuration parameters
   * @param {Boolean} config.recursive (optional) Process children or not
   * @param {Object} config.storeAcl (optional) See example
   * @param {Boolean} config.asAdmin (optional) If `true` and admin context is available it will be used to perform the task
   */
  addRights: function (objId, users, rights, config) {
    var me = this;

    me.logger.enter("addRights", arguments);
    me.editRights(objId, users, rights, config, me.addSordRights);
    me.logger.exit("addRights");
  },

  /**
   * Removes rights from an archive entry/entries.
   *
   * The following example will remove all rights (exept for read) for all users, having access to the object (and all sub-objects)
   * and store the original right to an map field (OLC_ACL) for a later restore.
   *
   *     sol.common.AclUtils.removeRights(
   *       "4711",
   *       [],
   *       { read: false, write: true, del: true, edit: true, list: true, perm: true },
   *       { recursive: true, storeAcl: { type: "MAP", key: "OLD_ACL" } }
   *     );
   *
   * @param {String} objId The object which should be edited (if config has a `recursive` flag set to `true`, this object will be the starting point)
   * @param {String[]|Object[]} users If this contains strings, they serve as user names. If it contains objects, they have to contain a `name` property and an `rights` object. If empty, all existing ACL entries will be edited.
   * @param {Object} rights Object with flags for each right that should be removed
   * @param {Object} config (optional) Additional configuration parameters
   * @param {Boolean} config.recursive (optional) Process children or not
   * @param {Object} config.storeAcl (optional) See example
   * @param {Boolean} config.asAdmin (optional) If `true` and admin context is available it will be used to perform the task
   */
  removeRights: function (objId, users, rights, config) {
    var me = this;

    me.logger.enter("removeRights", arguments);
    // for backwards compatibility: remove p-right if nothing is specified
    if (!rights.hasOwnProperty("perm") && !rights.hasOwnProperty("p")) {
      rights.p = true;
    }
    me.editRights(objId, users, rights, config, me.removeSordRights);
    me.logger.exit("removeRights");
  },


  /**
   * Sets rights to an archive entry/entries.
   *
   * The following example grants read, write and permission rights to the users "baum" and "renz" on exactly one object
   *
   *     sol.common.AclUtils.setRights(
   *       "4711",
   *       ["baum", "renz"],
   *       { r: true, w: true, d: false, e: false, l: false, p: true},
   *       { }
   *     );
   *
   * @param {String} objId The object which should be edited (if config has a `recursive` flag set to `true`, this object will be the starting point)
   * @param {String[]|Object[]} users If this contains strings, they serve as user names. If it contains objects, they have to contain a `name` property and an `rights` object. If empty, all existing ACL entries will be edited.
   * @param {Object} rights Object with flags for each right that should be added
   * @param {Object} config (optional) Additional configuration parameters
   * @param {Boolean} config.recursive (optional) Process children or not
   * @param {Object} config.storeAcl (optional) See example
   * @param {Boolean} config.asAdmin (optional) If `true` and admin context is available it will be used to perform the task
   */
  setRights: function (objId, users, rights, config) {
    var me = this;

    me.logger.enter("setRights", arguments);
    me.editRights(objId, users, rights, config, me.setSordRights);
    me.logger.exit("setRights");
  },

  /**
   * Restores (saved) rights for an archive entry/entries.
   *
   * The following example will restore all rights which where stored to the specified map field.
   *
   *     sol.common.AclUtils.restoreRights(
   *       "4711",
   *       { recursive: true, storeAcl: { type: "MAP", key: "OLD_ACL" } }
   *     );
   *
   * @param {String} objId The object which should be edited (if config has a `recursive` flag set to `true`, this object will be the starting point)
   * @param {Object} config (optional) Additional configuration parameters
   * @param {Boolean} config.recursive (optional) Process children or not
   * @param {Object} config.storeAcl (optional) See example
   * @param {Boolean} config.asAdmin (optional) If `true` and admin context is available it will be used to perform the task
   */
  restoreRights: function (objId, config) {
    var me = this,
        elements;
    me.logger.enter("restoreRights", arguments);

    if (config && config.storeAcl) {
      elements = me.retrieveElements(objId, config.recursive, config.asAdmin, config);

      elements.forEach(function (sord) {
        me.restoreSordRights(sord, config);
      });
    }
    me.logger.exit("restoreRights");
  },

  /**
   * @private
   * @param {String} objId
   * @param {Array} users
   * @param {Object} rights
   * @param {Object} config
   * @param {Function} combineAclFunction
   */
  editRights: function (objId, users, rights, config, combineAclFunction) {
    var me = this,
        accessCode, asAdmin, recursive, storeAcl, userAcl, elements;

    accessCode = me.createAccessCode(rights);
    asAdmin = (config && config.asAdmin === true) ? true : false;
    recursive = (config && config.recursive === true) ? true : false;
    storeAcl = (config && config.storeAcl) ? config.storeAcl : null;
    userAcl = me.retrieveUserAcl(users, accessCode, asAdmin);
    elements = me.retrieveElements(objId, recursive, asAdmin, config);

    elements.forEach(function (sord) {
      me.editSordRights(sord, { combineAclFunction: combineAclFunction, newAclList: userAcl, accessCode: accessCode, storeAcl: storeAcl, asAdmin: asAdmin });
    });
  },

  /**
   * @private
   * @param {String[]|Object[]} users If this contains strings, they serve as user names. If it contains objects, they have to contain a `name` property and an `rights` object
   * @param {Number} accessCode
   * @param {Boolean} asAdmin
   * @return {de.elo.ix.client.AclItem[]}
   */
  retrieveUserAcl: function (users, accessCode, asAdmin) {
    var me = this,
        connection, tmpAccessCode, userNames, userAcl, userInfos, userInfo, i, max;

    connection = ((asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;
    tmpAccessCode = null;

    if (users && users.length > 0) {
      userAcl = [];
      userNames = users.map(function (user) {
        return (user && user.hasOwnProperty && user.hasOwnProperty("name")) ? user.name : user + "";
      });

      userInfos = connection.ix().checkoutUsers(userNames, CheckoutUsersC.BY_IDS, LockC.NO);

      for (i = 0, max = userInfos.length; i < max; i++) {
        userInfo = userInfos[i];
        if (users[i] && users[i].hasOwnProperty && users[i].hasOwnProperty("rights")) {
          tmpAccessCode = me.createAccessCode(users[i].rights);
        }
        userAcl.push(me.createAclItemFromUserInfo(userInfo, tmpAccessCode || accessCode));
        tmpAccessCode = null;
      }
      return userAcl;
    }
    return null;
  },

  /**
   * @private
   * @param {Object} andGroup An and-group definition (see {@link #changeRightsInBackground})
   * @param {Number} defaultAccessCode
   * @param {Boolean} asAdmin
   * @return {de.elo.ix.client.AclItem}
   */
  retrieveAndGroupAcl: function (andGroup, defaultAccessCode, asAdmin) {
    var me = this,
        connection, groupsAccessCode, groupNames, groupInfos, userInfo, i, max,
        aclGroupsItem, idNameItem, idNames;

    connection = ((asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;

    if (andGroup.groups && (andGroup.groups.length > 1)) {
      groupNames = andGroup.groups.map(function (group) {
        return (group && group.hasOwnProperty && group.hasOwnProperty("name")) ? group.name : group;
      });

      groupInfos = connection.ix().checkoutUsers(groupNames, CheckoutUsersC.BY_IDS, LockC.NO);
      if (andGroup.rights) {
        groupsAccessCode = me.createAccessCode(andGroup.rights);
      }

      aclGroupsItem = new AclItem();
      aclGroupsItem.id = groupInfos[0].id;
      aclGroupsItem.type = AclItemC.TYPE_GROUP;
      aclGroupsItem.access = groupsAccessCode || defaultAccessCode;
      idNames = [];
      for (i = 1, max = groupInfos.length; i < max; i++) {
        userInfo = groupInfos[i];
        // eslint-disable-next-line no-undef
        idNameItem = new IdName();
        idNameItem.name = String(userInfo.name);
        idNameItem.guid = String(userInfo.guid);
        idNameItem.id = String(userInfo.id);
        idNames.push(idNameItem);
      }
      aclGroupsItem.andGroups = idNames;
      return aclGroupsItem;
    }
    return null;
  },

  /**
   * @private
   * @param {de.elo.ix.client.Sord} sord
   * @param {Number} accessCode
   * @return {de.elo.ix.client.AclItem[]}
   */
  retrieveSordAcl: function (sord, accessCode) {
    var me = this,
        sordAcl, i, aclItem;
    me.logger.enter("retrieveSordAcl", arguments);
    sordAcl = [];

    if (sord && (sord instanceof Sord)) {
      for (i = 0; i < sord.aclItems.length; i++) {
        aclItem = sord.aclItems[i];
        sordAcl.push(me.createAclItemFromAcl(aclItem, accessCode));
      }
    }
    me.logger.exit("retrieveSordAcl", sordAcl);
    return sordAcl;
  },

  /**
   * @private
   * @param {String} objId
   * @param {Boolean} recursive
   * @param {Boolean} asAdmin   *
   * @param {*} config
   * @return {de.elo.ix.client.Sord[]}
   */
  retrieveElements: function (objId, recursive, asAdmin, config) {
    var me = this,
        connection = ((asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect,
        elements = [connection.ix().checkoutSord(objId, me.sordZ, LockC.NO)],
        createFindChildrenConfig = function (aConfig) {
          var findConfig = sol.common.ObjectUtils.clone(aConfig.findChildren || {});

          findConfig.includeFolders = typeof findConfig.includeFolders != "undefined" ? findConfig.includeFolders : true;
          findConfig.includeDocuments = typeof findConfig.includeDocuments != "undefined" ? findConfig.includeDocuments : true;
          findConfig.sordZ = me.sordZ;
          findConfig.recursive = typeof findConfig.recursive != "undefined" ? findConfig.recursive : true;
          findConfig.level = typeof findConfig.level != "undefined" ? findConfig.level : -1;

          return findConfig;
        };

    if (recursive === true) {
      return elements.concat(sol.common.RepoUtils.findChildren(
        objId,
        createFindChildrenConfig(config),
        connection
      ) || []);
    }
    return elements;
  },

  /**
   * @private
   * @param {de.elo.ix.client.AclItem[]} oldAclList
   * @param {de.elo.ix.client.AclItem[]} newAclList
   * @param {Boolean} asAdmin
   * @return {de.elo.ix.client.AclItem[]}
   */
  addSordRights: function (oldAclList, newAclList, asAdmin) {
    var connection, _result;

    connection = ((asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;
    _result = connection.ix().combineAcl(oldAclList, newAclList, null).sum;
    return _result;
  },

  /**
   * @private
   * @param {de.elo.ix.client.AclItem[]} _oldAclList is not used here
   * @param {de.elo.ix.client.AclItem[]} newAclList
   * @param {Boolean} asAdmin
   * @return {de.elo.ix.client.AclItem[]}
   */
  setSordRights: function (_oldAclList, newAclList, asAdmin) {
    var connection;

    connection = ((asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;
    return connection.ix().combineAcl(newAclList, null, null).sum;
  },

  /**
   * @private
   * @param {de.elo.ix.client.AclItem[]} oldAclList
   * @param {de.elo.ix.client.AclItem[]} newAclList
   * @param {Boolean} asAdmin
   * @return {de.elo.ix.client.AclItem[]}
   */
  removeSordRights: function (oldAclList, newAclList, asAdmin) {
    var connection, _result;

    connection = ((asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;
    _result = connection.ix().combineAcl(oldAclList, newAclList, null).difference;
    return _result;
  },

  /**
   * @private
   * @param {de.elo.ix.client.Sord[]} sord
   * @param {Object} params
   */
  editSordRights: function (sord, params) {
    var me = this,
        connection, oldAclList, oldAclString;

    connection = ((params.asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;
    oldAclList = sord.aclItems;
    oldAclString = sord.acl;

    if (params.storeAcl) {
      switch (params.storeAcl.type) {
        case "MAP":
          connection.ix().checkinMap(MapDomainC.DOMAIN_SORD, sord.id, sord.id, [new KeyValue(params.storeAcl.key, oldAclString)], LockC.NO);
          break;
        case "GRP":
          throw "store ACL to group is not implemented yet";
        default:
          throw "unkown field type";
      }
    }

    if (!params.newAclList) {
      params.newAclList = me.retrieveSordAcl(sord, params.accessCode);
    }

    sord.aclItems = params.combineAclFunction(oldAclList, params.newAclList, params.asAdmin);

    connection.ix().checkinSord(sord, me.sordZ, LockC.NO);
  },

  /**
   * @private
   * @param {de.elo.ix.client.Sord} sord
   * @param {Object} params
   */
  restoreSordRights: function (sord, params) {
    var me = this,
        connection, mapItems;

    connection = ((params.asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;

    if (params.storeAcl.type === "MAP") {
      mapItems = connection.ix().checkoutMap(MapDomainC.DOMAIN_SORD, sord.id, [params.storeAcl.key], LockC.NO).items;
      if (mapItems && mapItems.length === 1) {
        sord.acl = mapItems[0].value;
        sord.aclItems = null;
        connection.ix().checkinSord(sord, me.sordZ, LockC.NO);
        connection.ix().deleteMap(MapDomainC.DOMAIN_SORD, sord.id, [params.storeAcl.key], LockC.NO);
      }
    }
  },

  /**
   * @private
   * @param {de.elo.ix.client.UserInfo} userInfo
   * @param {Number} accessCode
   * @return {de.elo.ix.client.AclItem}
   */
  createAclItemFromUserInfo: function (userInfo, accessCode) {
    var aclItem;

    aclItem = new AclItem();
    aclItem.id = userInfo.id;
    aclItem.type = (userInfo.type == UserInfoC.TYPE_GROUP) ? AclItemC.TYPE_GROUP : AclItemC.TYPE_USER;
    aclItem.access = accessCode;
    return aclItem;
  },

  /**
   * @private
   * @param {de.elo.ix.client.AclItem} aclItem
   * @param {Number} accessCode
   * @return {de.elo.ix.client.AclItem}
   */
  createAclItemFromAcl: function (aclItem, accessCode) {
    var me = this,
        newAclItem, i, newAndGroups, idName, newIdName;
    me.logger.enter("createAclItemFromAcl", arguments);

    newAclItem = new AclItem();
    newAclItem.id = aclItem.id;
    newAclItem.type = aclItem.type;
    newAclItem.access = accessCode;

    if (aclItem.andGroups && (aclItem.andGroups.length > 0)) {
      newAndGroups = [];
      for (i = 0; i < aclItem.andGroups.length; i++) {
        idName = aclItem.andGroups[i];
        // eslint-disable-next-line no-undef
        newIdName = new IdName();
        newIdName.id = idName.id;
        newIdName.name = idName.name;

        newAndGroups.push(newIdName);
      }
      newAclItem.andGroups = newAndGroups;
    }

    me.logger.exit("createAclItemFromAcl", newAclItem);
    return newAclItem;
  },

  /**
   * @private
   * @param {Object} rights
   * @return {Number}
   */
  createAccessCode: function (rights) {
    var me = this,
        code;

    code = 0;
    if (!rights) {
      throw "Rights are empty";
    }

    if ((rights.read === true) || (rights.r === true)) {
      code |= AccessC.LUR_READ;
    }
    if ((rights.write === true) || (rights.w === true)) {
      code |= AccessC.LUR_WRITE;
    }
    if ((rights.del === true) || (rights.d === true)) {
      code |= AccessC.LUR_DELETE;
    }
    if ((rights.edit === true) || (rights.e === true)) {
      code |= AccessC.LUR_EDIT;
    }

    if ((rights.list === true) || (rights.l === true)) {
      code |= AccessC.LUR_LIST;
    }

    if (((rights.perm === true) || (rights.p === true)) && (me.isAccessCodePermissionAvailable())) {
      code |= AccessC.LUR_PERMISSION;
    }

    return code;
  },

  /**
   * Is the access code `permission` available
   * Check must be compatible with Rhino, Nashorn and GraalVM
   *
   * @returns {Boolean}
   */
  isAccessCodePermissionAvailable: function () {
    var me = this,
        accessC, accessCClass;

    if (typeof me.accessCodePermissionAvailable == "undefined") {
      accessC = new AccessC;
      accessCClass = accessC.getClass();
      me.accessCodePermissionAvailable = me.hasClassField(accessCClass, "LUR_PERMISSION");
    }

    return me.accessCodePermissionAvailable;
  },

  /**
   * @private
   * @param {java.lang.Class} clazz
   * @param {String} fieldName
   * @return {Boolean} Has class field
   */
  hasClassField: function (clazz, fieldName) {
    var fields, i, field;

    fields = clazz.getDeclaredFields();

    for (i = 0; i < fields.length; i++) {
      field = fields[i];
      if (field.name == fieldName) {
        return true;
      }
    }

    return false;
  },

  /**
   * Sets or adds the rights by a background IX thread.
   *
   * Examples:
   *
   *     var jobState = sol.common.AclUtils.changeRightsInBackground("ARCPATH:/AclTest/Acl1", { inherit: true, users: ["weiler", {name: "zipfel", rights:{r: true, w: true, p: true}}], rights: { r: true } });
   *     var jobState = sol.common.AclUtils.changeRightsInBackground("ARCPATH:/AclTest/Acl1", { mode: "SET", users: ["zipfel"], rights: { r: true } });
   *     var jobState = sol.common.AclUtils.changeRightsInBackground("ARCPATH:/AclTest/Acl1", { mode: "SET", users: ["weiler", { name: "zipfel", rights:{ r: true, w: true } }], rights: { r: true }, andGroups: { groups: ["Pubsec.Registratur", { name: "Pubsec.Sachbearbeiter" }], rights: { d: true } } });
   *     var jobState = sol.common.AclUtils.changeRightsInBackground("ARCPATH:/AclTest/Acl1", { mode: "ADD", users: [{ "type": "GRP", "key": "CONTRACT_RESPONSIBLE", "rights": { "r": true, "w": true, "d": false, "e": false, "l": false } }] });
   *     var jobState = sol.common.AclUtils.changeRightsInBackground("ARCPATH:/AclTest/Acl1", { mode: "ADD", users: [{ "type": "GRP", "key": "CONTRACT_RESPONSIBLE", "mode": "SUPERVISOR", "supervisorLevel": 0, "rights": { "r": true, "w": true, "d": false, "e": false, "l": false } }] });
   *
   * Example with and-groups
   * (sets an and-group with the groups 'GroupA', 'GroupB' and the group from the CONTRACT_RESPONSIBLE field with read only,
   * as well as an and-group with the groups 'GroupX' and 'GroupY' with write access):
   *
   *     var jobState = sol.common.AclUtils.changeRightsInBackground("ARCPATH:/AclTest/Acl1", {
   *       mode: "SET",
   *       andGroups: [
   *         { groups: ["GroupA", "GroupB", { type: "GRP", key: "CONTRACT_RESPONSIBLE" }] },
   *         { groups: ["GroupX", "GroupY"], rights: { r: true, w: true } }
   *       ],
   *       rights: { r: true } // default rights
   *     });
   *
   * User or group names can be created using handlbars syntax (if the value will be read from an indexfield, that value could also contain handlebars syntax):
   *
   *     var jobState = sol.common.AclUtils.changeRightsInBackground("ARCPATH:/AclTest/Acl1", {
   *       mode: "ADD",
   *       users: [
   *         "LEGAL_DEP_{{sord.objKeys.CONTRACT_DEPARTMENT}}",
   *         { name: "CONTROLLING_{{sord.objKeys.CONTRACT_DEPARTMENT}}", rights: { r: true, w: true } }
   *       ],
   *       rights: { r: true } // default rights
   *     });
   *
   * @param {String|String[]} objId Object ID of the object which will be changed or an array with start ids where each will be prosessed. If an array of ids is used, the parameter `config.srcObjId` is mandatory.
   * @param {Object} config Configuration
   * @param {String} [config.mode="ADD"] "ADD", "SET" or "REMOVE" rights
   * @param {Object} config.inherit Inheritance configuration
   * @param {Boolean} config.inherit.fromDirectParent Inherit the ACL from the direct parent (default)
   * @param {Boolean} config.inherit.aclSrcObjId Source object ID for the ACL inheritance
   * @param {String[]} config.inherit.solutionObjectTypes Solution object types to find the ACL source object in the hierachy
   * @param {String[]|Object[]} config.users If this contains strings, they serve as user names. If it contains objects, they have to contain a `name` property and an `rights` object
   * @param {Object[]} config.andGroups An array with and-group definitions
   * @param {String[]|Object[]} config.andGroups.groups The groups contained in an and-group. Defined in the same way as `config.users` (but without the `rights` property).
   * If the `groups` array contains less than two entries the that and-group definition will be ignored.
   * @param {Object[]} config.andGroups.rights The rights of an and-group if deviant from fallback (`config.rights`)
   * @param {String} config.srcObjId If set, the user configurations will be read from this object instead of `objId`. Only mandatory if `objId` is an array.
   * @param {Object} config.rights Additional rights, e.g. { read: false, write: true, del: true, edit: true, list: true, perm: true }
   * @param {Boolean} [config.recursive=true] If true the ACL of the children will also be changed. Default is true.
   * @param {Boolean} [config.dontWait=false] Don't wait for the background process. Default is false (synchronous).
   * @param {String} config.flowId Flow ID to determine the values of flow map fields
   * @return {de.elo.ix.client.JobState}
   */
  changeRightsInBackground: function (objId, config) {
    var me = this,
        logObj = { objId: String(objId), config: config },
        newAclItems = [],
        conn = (typeof ixConnectAdmin !== "undefined") ? ixConnectAdmin : ixConnect,
        processObjIds, metadataObjId, defaultAccessCode, jobState;

    me.logger.enter("changeRightsInBackground", logObj);

    config = config || {};

    me.checkPreconditions(objId, config);

    processObjIds = (sol.common.ObjectUtils.isArray(objId)) ? objId : [objId];
    metadataObjId = config.srcObjId || objId;

    me.initializeRights(newAclItems, metadataObjId, config, conn);

    defaultAccessCode = me.createAccessCode(config.rights);

    me.appendInheritedAcl(newAclItems, metadataObjId, config, conn);
    me.appendUserAcl(newAclItems, metadataObjId, config, defaultAccessCode);
    me.appendAndGroupAcl(newAclItems, metadataObjId, config, defaultAccessCode);

    if (me.canExecute(config.mode, newAclItems)) {
      jobState = me.executeBackgroundAclJob(conn, processObjIds, config, newAclItems);
    } else {
      jobState = null;
      me.logger.warn("no acl items to set/add/remove -> skip processing");
    }

    me.logger.exit("changeRightsInBackground", jobState);

    return jobState;
  },

  /**
   * @private
   * Checks the preconditions and throws an exception if they are not meet.
   * @param {String|String[]} objId
   * @param {Object} config
   */
  checkPreconditions: function (objId, config) {
    if (!objId) {
      throw "Object ID is empty";
    }

    if (sol.common.ObjectUtils.isArray(objId) && !config.srcObjId) {
      throw "If 'objId' is configured as array, the config paramater 'srcObjId' is mandatory.";
    }
  },

  /**
   * @private
   * Checks and initializes the rights config.
   * @param {de.elo.ix.client.AclItem[]} newAclItems
   * @param {String} objId
   * @param {Object} config
   * @param {de.elo.ix.client.IXConnection} conn
   */
  initializeRights: function (newAclItems, objId, config, conn) {
    var sord;
    if (config.mode == "REMOVE") {
      config.rights = config.rights || { read: false, write: true, del: true, edit: true, list: true, perm: true };
      if (!config.users) {
        sord = conn.ix().checkoutSord(objId, new SordZ(SordC.mbAclItems), LockC.NO);
        newAclItems = sord.aclItems;
      }
    } else {
      config.rights = config.rights || { read: true, write: true, del: true, edit: true, list: true, perm: true };
      if (!config.users && !config.andGroups && !config.inherit) {
        throw "Users, andGroups or inheritance must be set";
      }
    }
  },

  /**
   * @private
   * Appends the inherited ACL items to the `newAclItems` array as configured.
   * @param {de.elo.ix.client.AclItem[]} newAclItems
   * @param {String} objId
   * @param {Object} config
   * @param {de.elo.ix.client.IXConnection} conn
   */
  appendInheritedAcl: function (newAclItems, objId, config, conn) {
    var aclSrcSord, sord, i, aclItem;

    if (config.inherit) {
      if (config.inherit.aclSrcObjId) {
        aclSrcSord = conn.ix().checkoutSord(config.inherit.aclSrcObjId, new SordZ(SordC.mbAclItems), LockC.NO);
      } else if (config.inherit.solutionObjectTypes) {
        aclSrcSord = sol.common.RepoUtils.findObjectTypeInHierarchy(objId, config.inherit.solutionObjectTypes, { connection: conn });
      } else {
        sord = conn.ix().checkoutSord(objId, SordC.mbMin, LockC.NO);
        if (sord.parentId > 0) {
          aclSrcSord = conn.ix().checkoutSord(sord.parentId, new SordZ(SordC.mbAclItems), LockC.NO);
        }
      }

      if (aclSrcSord) {
        for (i = 0; i < aclSrcSord.aclItems.length; i++) {
          aclItem = aclSrcSord.aclItems[i];
          newAclItems.push(aclItem);
        }
      }
    }
  },

  /**
   * @private
   * Appends the user ACL items to the `newAclItems` array as configured.
   * @param {de.elo.ix.client.AclItem[]} newAclItems
   * @param {String} objId
   * @param {Object} config
   * @param {Number} defaultAccessCode
   */
  appendUserAcl: function (newAclItems, objId, config, defaultAccessCode) {
    var me = this,
        users, userAcls;
    users = me.preprocessUsers(objId, config.users, config);
    userAcls = me.retrieveUserAcl(users, defaultAccessCode);
    if (userAcls) {
      userAcls.forEach(function (userAcl) {
        newAclItems.push(userAcl);
      });
    }
  },

  /**
   * @private
   * Appends the and-group ACL items to the `newAclItems` array as configured.
   * @param {de.elo.ix.client.AclItem[]} newAclItems
   * @param {String} objId
   * @param {Object} config
   * @param {Number} defaultAccessCode
   */
  appendAndGroupAcl: function (newAclItems, objId, config, defaultAccessCode) {
    var me = this;
    if (config.andGroups && (config.andGroups.length > 0)) {
      config.andGroups.forEach(function (andGroup) {
        var andGroupAcl;
        andGroup.groups = me.preprocessUsers(objId, andGroup.groups, config);
        andGroupAcl = me.retrieveAndGroupAcl(andGroup, defaultAccessCode);
        if (andGroupAcl) {
          newAclItems.push(andGroupAcl);
        }
      });
    }
  },

  /**
   * @private
   * Checks, if the background processing should be executed.
   *
   * If mode is `SET` or `REPLACE` this always returns `true`, else it checks if there are acl items.
   * @param {String} mode
   * @param {Object[]} aclItems
   * @return {Boolean}
   */
  canExecute: function (mode, aclItems) {
    if ((mode == "SET") || (mode == "REPLACE")) {
      return true;
    }

    return aclItems && (aclItems.length > 0);
  },

  /**
   * @private
   * Executes the background processing of the rights.
   * @param {de.elo.ix.client.IXConnection} conn
   * @param {String[]} startIds
   * @param {Object} config
   * @param {de.elo.ix.client.AclItem[]} newAclItems
   * @return {de.elo.ix.client.JobState}
   */
  executeBackgroundAclJob: function (conn, startIds, config, newAclItems) {
    var me = this,
        procInfo, navInfo, jobState;

    me.logger.enter("executeBackgroundAclJob");

    procInfo = new ProcessInfo();
    procInfo.desc = (config.mode || "ADD") + " ACL";
    procInfo.errorMode = ProcessInfoC.ERRORMODE_SKIP_PROCINFO;
    procInfo.procAcl = new ProcessAcl();

    if ((config.mode == "SET") || (config.mode == "REPLACE")) {
      procInfo.procAcl.setAclItems = newAclItems;
    } else if (config.mode == "REMOVE") {
      procInfo.procAcl.subAclItems = newAclItems;
    } else {
      procInfo.procAcl.addAclItems = newAclItems;
    }

    navInfo = new NavigationInfo();
    navInfo.startIDs = startIds;
    navInfo.maxDepth = (config.recursive === false) ? 1 : 0;

    jobState = conn.ix().processTrees(navInfo, procInfo);

    if (!config.dontWait && !me.ixExecutesBackgroundJobsSynchronous(conn)) {
      jobState = sol.common.AsyncUtils.waitForJob(jobState.jobGuid, { connection: conn });
    }

    me.logger.exit("executeBackgroundAclJob", jobState);

    return jobState;
  },

  /**
   * @private
   * Checks if the IX version processes background jobs synchronous.
   * This is true for IX version higher than '9.18.060', '10.18.060' and '11.01.000'.
   * @param {de.elo.ix.client.IXConnection} conn The connection wich is used to determine the IX version.
   * @return {Boolean}
   */
  ixExecutesBackgroundJobsSynchronous: function (conn) {
    return sol.common.RepoUtils.checkVersions(conn.clientVersion, ["9.18.060", "10.18.060", "11.01.000"]);
  },

  /**
   * Preprocesses users.
   * Retrieves user names from index fields.
   * If a user name or the retrieved value contains handlebars syntax, the sord will be applied to that string.
   * @param {String} objId Object ID
   * @param {String[]|Number[]|Object[]} users Array of user defintions. all Types can be mixed.
   * @param {Object} params Parameters
   * @param {String} params.flowId Workflow ID
   * @return {Object[]}
   *
   * # Example of the parameter 'users':
   *     [
   *       "mustermannm",
   *       23,
   *       { "type": "GRP", "key": "CONTRACT_RESPONSIBLE", "rights": { "r": true, "w": true, "d": false, "e": false, "l": false, "p": true } },
   *       { "type": "GRP", "key": "CONTRACT_DEPARTMENT", "rights": { "r": true, "w": true, "d": false, "e": false, "l": false, "p": true } },
   *       { "type": "GRP", "key": "CONTRACT_PROCUREMENT", "rights": { "r": true, "w": true, "d": false, "e": false, "l": false, "p": false } },
   *       { "name": "LEGAL_DEP_{{sord.objKeys.CONTRACT_DEPARTMENT}}", "rights": { "r": true, "w": false, "d": false, "e": false, "l": false, "p": false } }
   *     ]
   */
  preprocessUsers: function (objId, users, params) {
    var me = this,
        ctxSord = { objId: objId },
        conn, userCfgs;

    conn = (typeof ixConnectAdmin !== "undefined") ? ixConnectAdmin : ixConnect;

    userCfgs = [];
    if (users && (users.length > 0)) {
      users = JSON.parse(sol.common.JsonUtils.stringifyAll(users));
      users.forEach(function (user) {
        var userNames = [],
            rawUserNames, rawUserName, userName, i, supervisorLevel, userInfos, sord;

        if (sol.common.ObjectUtils.isString(user) || sol.common.ObjectUtils.isNumber(user)) {
          user = me.replaceUserNamePlaceholders(String(user), ctxSord, conn, params.flowId);
          if (user) {
            userCfgs.push(user);
          }
        } else if (!user.name && user.type && user.key) {
          try {
            sord = me.enrichContextSord(ctxSord, false, conn, params.flowId).sord;

            if ((user.type == "WFMAP") && (params.flowId)) {
              rawUserName = sol.common.WfUtils.getWfMapValue(params.flowId, user.key);
              rawUserNames = (rawUserName) ? [rawUserName] : [];
            } else {
              rawUserNames = sol.common.SordUtils.getValues(sord, user);
            }

            supervisorLevel = user.supervisorLevel || 0;

            rawUserNames = rawUserNames || [];

            for (i = 0; i < rawUserNames.length; i++) {
              rawUserName = rawUserNames[i];
              userName = rawUserName;

              if (user.mode && (user.mode.toUpperCase() == "SUPERVISOR")) {
                userName = sol.common.UserUtils.getSupervisorOfLevel(rawUserName, supervisorLevel);
                if (!userName) {
                  me.logger.debug("Can't find supervisor: userName={0}", rawUserName);
                }
              }

              if (userName) {
                userNames.push(userName);
              }
            }

            if (userNames.length > 0) {
              userInfos = ixConnect.ix().checkoutUsers(userNames, CheckoutUsersC.BY_IDS, LockC.NO);
            }

            if (userInfos && (userInfos.length > 0)) {
              userInfos.forEach(function (userInfo) {
                var tmpUser = JSON.parse(sol.common.JsonUtils.stringifyAll(user)); // copy current user configuration for each user in the list
                tmpUser.name = me.replaceUserNamePlaceholders(userInfo.name, ctxSord, conn, params.flowId);
                if (tmpUser) {
                  userCfgs.push(tmpUser);
                }
              });
            }
          } catch (ignore) {
            me.logger.debug("error determining user(s)", ignore);
          }
        } else {
          user.name = me.replaceUserNamePlaceholders(user.name, ctxSord, conn, params.flowId);
          if (user && user.name) {
            userCfgs.push(user);
          }
        }
      });

      me.logger.debug(["preprocessUsers(): resolvedUsers={0}", JSON.stringify(userCfgs)]);

      return userCfgs;
    }
  },

  /**
   * @private
   * Replace user name place holders:
   *
   * - If `userName` equals '$CURRENTUSER' the current session user will be used.
   * - If `username` contains handlebars syntax, the sord will be applied to it
   *
   * @param {String} userName
   * @param {Object} ctxSord (optional) only used if `userName` contains handlebars syntax
   * @param {de.elo.ix.client.IXConnection} conn (optional) only used if `userName` contains handlebars syntax
   * @param {String} flowId (optional)
   * @return {String} Username
   */
  replaceUserNamePlaceholders: function (userName, ctxSord, conn, flowId) {
    var me = this,
        tplSord;

    userName = (userName || "") + "";

    if (userName == "$CURRENTUSER") {
      return String(ixConnect.loginResult.user.name);
    }
    if (userName.indexOf("{{") > -1) {
      tplSord = me.enrichContextSord(ctxSord, true, conn, flowId).tplSord;
      return sol.create("sol.common.Template", { source: userName }).apply(tplSord);
    }
    return userName;
  },

  /**
   * @private
   * Prefills the context object with sord and if requested with a template sord.
   * @param {Object} ctxSord
   * @param {Boolean} inclTplSord
   * @param {de.elo.ix.client.IXConnection} conn
   * @param {String} flowId (optional)
   * @return {Object} The ctxSord itself after enrichment
   */
  enrichContextSord: function (ctxSord, inclTplSord, conn, flowId) {
    if (!ctxSord.sord) {
      if (!ctxSord.objId) {
        throw "Object ID is empty";
      }
      ctxSord.sord = conn.ix().checkoutSord(ctxSord.objId, SordC.mbAllIndex, LockC.NO);
    }
    if (inclTplSord && !ctxSord.tplSord) {
      ctxSord.tplSord = flowId
        ? sol.common.WfUtils.getTemplateSord(ctxSord.sord, flowId)
        : sol.common.SordUtils.getTemplateSord(ctxSord.sord);
    }
    return ctxSord;
  },

  /**
   * Checks wether the users object contains the session user und the session user has already the effective rights
   * @param {Object} rightsConfig Rights configuration
   * @param {String} rightsConfig.objId Object ID
   * @param {Object} rightsConfig.rights Rights
   * @return {Boolean}
   */
  containsSessionUserAndhasEffectiveRights: function (rightsConfig) {
    var me = this,
        currUserName, effectiveRights;

    if (rightsConfig && rightsConfig.objId && rightsConfig.rights && rightsConfig.users && (rightsConfig.users.length == 1) && sol.common.ObjectUtils.isString(rightsConfig.users[0])) {
      currUserName = ixConnect.loginResult.user.name;
      if (rightsConfig.users[0] == currUserName) {
        effectiveRights = me.hasEffectiveRights(rightsConfig.objId, rightsConfig.rights);
        return effectiveRights;
      }
    }

    return false;
  },

  /**
   * Checks wether the current user has effective rights
   * @param {String|de.elo.ix.client.Sord} sord Object ID or Sord
   * @param {Object} params Parameters
   * @param {Object} params.rights Rights
   * @return {Boolean}
   */
  hasEffectiveRights: function (sord, params) {
    var me = this,
        hasRights = false, accessCode;

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

    params = params || {};
    params.rights = params.rights || { r: true, w: true, d: true, e: true, l: true, p: true };

    try {
      if (!(sord instanceof Sord) || !sord.aclItems) {
        sord = ixConnect.ix().checkoutSord(sord, new SordZ(SordC.mbAclItems), LockC.NO);
      }

      accessCode = sol.common.AclUtils.getAccessCode(sord);

      hasRights = me.containsRights(accessCode, params.rights);
    } catch (ignore) {
      // ignore
    }

    return hasRights;
  },

  accessCodes: {
    r: AccessC.LUR_READ,
    read: AccessC.LUR_READ,
    w: AccessC.LUR_WRITE,
    write: AccessC.LUR_WRITE,
    d: AccessC.LUR_DELETE,
    del: AccessC.LUR_DELETE,
    e: AccessC.LUR_EDIT,
    edit: AccessC.LUR_EDIT,
    l: AccessC.LUR_LIST,
    list: AccessC.LUR_LIST,
    p: new AccessC().LUR_PERMISSION, // 'new' is necessary, because it throws no exception if property does not exist (prior to ELO12)
    perm: new AccessC().LUR_PERMISSION // 'new' is necessary, because it throws no exception if property does not exist (prior to ELO12)
  },

  /**
   * Checks wether the access code contains the requested rights
   * @param {Number} accessCode Access code
   * @param {Object} rights Rights
   * @return {Boolean}
   */
  containsRights: function (accessCode, rights) {
    var me = this,
        rightKey, accessFlag;

    if (!accessCode || !rights) {
      return true;
    }

    for (rightKey in rights) {
      if (rights.hasOwnProperty(rightKey)) {
        accessFlag = me.accessCodes[rightKey];
        if (accessFlag && ((accessCode & accessFlag) == 0)) {
          return false;
        }
      }
    }

    return true;
  },

  /**
   * Returns an object with the access rights of the current user on a sord.
   * @param {de.elo.ix.client.Sord} sord
   * @returns {Object}
   */
  getAccessRights: function (sord) {
    var accessCode;

    accessCode = sol.common.AclUtils.getAccessCode(sord);
    return sol.common.AclUtils.getAccessRightsByAccessCode(accessCode);
  },

  /**
   * Returns an object with the access rights of the given accessCode.
   * @param {number} accessCode
   * @returns {Object}
   */
  getAccessRightsByAccessCode: function (accessCode) {
    var accessRights = {
      r: sol.common.AclUtils.containsRights(accessCode, { r: true }),
      w: sol.common.AclUtils.containsRights(accessCode, { w: true }),
      d: sol.common.AclUtils.containsRights(accessCode, { d: true }),
      e: sol.common.AclUtils.containsRights(accessCode, { e: true }),
      l: sol.common.AclUtils.containsRights(accessCode, { l: true }),
      p: sol.common.AclUtils.containsRights(accessCode, { p: true })
    };

    return accessRights;
  },

  /**
   * Returns the access code of the given sord object
   * @param {String|de.elo.ix.client.Sord} sord Object ID or Sord
   * @param {Object} params Parameters
   * @param {de.elo.ix.client.IXConnection} params.connection Index server connection
   * @returns {number} access code
   */
  getAccessCode: function (sord, params) {
    var aclAccessInfo, conn, access;

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

    conn = params.connection;

    aclAccessInfo = new AclAccessInfo();
    if (!(sord instanceof Sord) || !sord.aclItems) {
      sord = conn.ix().checkoutSord(sord, new SordZ(SordC.mbAclItems), LockC.NO);
    }
    aclAccessInfo.aclItems = sord.aclItems;
    access = conn.ix().getAclAccess(aclAccessInfo).access;

    return access;
  }
});