    importPackage(Packages.de.elo.ix.client);

    //@include lib_Class.js
    //@include lib_sol.common.IxUtils.js
    //@include lib_sol.common.Template.js
    //@include lib_sol.common.Injection.js
    //@include lib_sol.common.UserUtils.js
    //@include lib_sol.common.JsonUtils.js
    //@include lib_sol.common.SordUtils.js
    //@include lib_sol.common.ObjectUtils.js
    //@include lib_sol.common.ix.FunctionBase.js
    //@include lib_sol.common.ObjectFormatter.MapTableToArray.js

    var logger = sol.create("sol.Logger", { scope: "sol.common.ix.functions.ChangeGroup" });

    /**
     * Changes the given ELO group.
     *
     * It is possible to add new members to the ELO group or remove existing members.
     *
     * When the user is already a member of the ELO group nothing will be happen
     *
     * ### Retrieving user list from mapTable
     *
     * In the example all userNames will be returned where the field MEETING_PERSON_CRUD_STATUS is
     * equal `CREATE`. With `propSelector` you can reach that the function returns an array with plain
     * strings instead of an object
     *
     *    "addMembers": [
    *        {
     *         "type": "WFMAP",
     *         "key": ["MEETING_PERSON_USERNAME", "MEETING_PERSON_CRUD_STATUS"],
     *         "table": true,
     *         "propSelector": "MEETING_PERSON_USERNAME",
     *         "filter": [
     *            { "prop": "MEETING_PERSON_CRUD_STATUS", "value": "CREATE" }
     *         ]
     *       }
     *     ]
     *
     *  Hint: You have to pass the flowId to the function to enable WFMAP fields
     *
     * # As workflow node
     *
     * ObjId is set based on the element that the workflow is attached to.
     * Following additional configuration can be applied to the comments field.
     *
     *     {
     *      "groupName": "New Group"
     *     }
     *
     * # As IX function call
     *
     * In addition to the workflow node configuration the objId must be passed.
     *
     *     sol.common.IxUtils.execute("RF_sol_function_ChangeGroup", {
     *       groupName: "group",
     *       addMembers: ["renz", "kraft"],
     *       removeMembers: ["eichner"]
     *     });
     *
     * @eloix
     * @requires sol.common.IxUtils
     * @requires sol.common.WfUtils
     * @requires sol.common.Template
     * @requires sol.common.Injection
     * @requires sol.common.ix.RfUtils
     * @requires sol.common.ix.JsonUtils
     * @requires sol.common.ix.ObjectUtils
     * @requires sol.common.ix.FunctionBase
     * @requires sol.common.ObjectFormatter.MapTableToArray
     *
     */
    sol.define("sol.common.ix.functions.ChangeGroup", {
      extend: "sol.common.ix.FunctionBase",

      /**
       * @cfg groupName
       */

       /**
       * @cfg {Boolean} [asAdmin=false] (optional)
       * When `asAdmin` is set to true the operation will be execute with an admin connection
       */

      mixins: ["sol.common.mixins.Inject"],

      inject: {
        name: { prop: "groupName", template: true },
        sord: { sordIdFromProp: "objId", flowIdFromProp: "flowId", optional: false }
      },

      TEMPLATESORDACCESSORS: {
        SORD: "",
        GRP: "objKeys.",
        MAP: "mapKeys.",
        WFMAP: "wfMapKeys."
      },

      /**
      * @cfg {String} groupName
      * The name of the new group. Templating is possible if objId/flowId is passed to the function
      */

      /**
      * @cfg {Array<Object>|Object|String} addMembers
      *
      */

      /**
       * @cfg {Array<Object>|Object|String} removeMembers
       */

       initialize: function (config) {
        var me = this;

        if (config.addMembers && !sol.common.ObjectUtils.isArray(config.addMembers)) {
          config.addMembers = [config.addMembers];
        }

        if (config.removeMembers && !sol.common.ObjectUtils.isArray(config.removeMembers)) {
          config.removeMembers = [config.removeMembers];
        }

        me.$super("sol.common.ix.FunctionBase", "initialize", [config]);
        me.checkConfig();
        me.connection = ((me.asAdmin === true) && (typeof ixConnectAdmin !== "undefined")) ? ixConnectAdmin : ixConnect;

      },

      checkConfig: function () {
        var me = this, flowIdIsNecessary,
          checkWorkflowIsNecessary = function (entry) {
            return entry && entry.type === "WFMAP";
          };

        flowIdIsNecessary = (me.addMembers || []).some(checkWorkflowIsNecessary)
          || (me.removeMembers || []).some(checkWorkflowIsNecessary);

        if (flowIdIsNecessary && !me.flowId) {
            throw Error("Your configuration contains WFMAP fields. `flowId` must be passed to the function");
        }
      },

      /**
       *
       */
      process: function () {
        var me = this, addMembers, removeMembers,
          groupName = me.getGroupName();

        try {
          if (me.shouldAddMembers()) {
            // add users to created group
            me.logger.debug(["addMembers {0}", JSON.stringify(me.addMembers)]);
            addMembers = me.prepareUserList(me.addMembers || []);

            if (!me.$dryRun) {
              me.addUsersToGroups(addMembers, [groupName]);
            }
          }

          if (me.shouldRemoveMembers()) {
            removeMembers = me.prepareUserList(me.removeMembers || []);

            if (!me.$dryRun) {
              me.removeUsersFromGroups(removeMembers, [groupName]);
            }
          }
        } catch (ex) {
          me.logger.warn(["An error occured {0}", ex.message], ex);
          throw ex;
        }

        return {
          name: groupName,
          addMembers: addMembers,
          removeMembers: removeMembers
        };
      },

      getGroupName: function () {
        var me = this, key;
        if (sol.common.ObjectUtils.isString(me.groupName)) {
          return me.groupName;
        } else if (me.groupName && me.groupName.type) {
           // complex object { type: "GRP", "key": "FIELDNAME" }
          if (!me.groupName || !me.hasAccesssorKey(me.groupName)) {
            throw Error("process type " + me.groupName.type + "is not supported currently");
          }

          key = me.getAccessor(me.groupName);
          return sol.common.ObjectUtils.getProp(me.sord, key);
        } else {
          throw Error("Could not determine groupName");
        }
      },

      /**
       * @private
       * @returns
       */
      shouldAddMembers: function () {
        var me = this;
        return me.addMembers && me.addMembers.length > 0;
      },

      /**
       * @private
       * @returns
       */
      shouldRemoveMembers: function () {
        var me = this;
        return me.removeMembers && me.removeMembers.length > 0;
      },

      /**
       * @private
       * @param {} names
       * @param {*} targetGroups
       */
       addUsersToGroups: function (names, targetGroups) {
        var me = this;
        me.logger.debug(["addUsersToGroups {0} {1}", names, targetGroups]);
        sol.common.UserUtils.addUsersToGroups(names, targetGroups, {
          connection: me.connection
        });
      },

      /**
       * @private
       * @param {} names
       * @param {*} targetGroups
       */
      removeUsersFromGroups: function (names, targetGroups) {
        var me = this;
        me.logger.debug(["removeUsersFromGroups {0} {1}", names, targetGroups]);
        sol.common.UserUtils.removeUsersFromGroups(names, targetGroups, {
          connection: me.connection
        });
      },

      prepareUserList: function (userDef) {
        var me = this, users, userList;

        users = (userDef || [])
          .filter(function (entry) { return entry; }) // TODO: implement default filter in ObjectUtils to filter nullable values
          .reduce(function (userObjList, entry) {
          if (sol.common.ObjectUtils.type(entry, "string")) {
            userList = [entry];
          } else if (entry.type) {
            userList = me.extractUsers(entry);
          } else if (entry.service) {
            throw Error("elementService is currently not implemented");
          } else {
            throw Error("Unknown entry type in `addUsersToGroup`. Only `type` and `service` is allowed");
          }

          // each user should be unqiue in the array so we store the user
          // into an object first. It avoids duplicates user.
          // User have to been a string value here
          userList && userList.reduce(function (objList, user) {
            user && (objList[user] = true);
            return objList;
          }, userObjList);

          return userObjList;
        }, {});

        // Now we convert it back to an array, so we can pass it easily to UserUtils
        return Object.keys(users);
      },

      /**
       *
       * @param {Object} entry
       * @param {Object} entry.type
       * @param {Object} entry.key
       * @returns {Array<String>}
       */
      extractUsers: function (entry) {
        var me = this, key,
          mapTableFormatter, users, output, keys;

        // complex object { type: "GRP", "key": "FIELDNAME" }
        if (!entry || !me.hasAccesssorKey(entry)) {
          throw Error("process type " + entry.type + "is not supported currently");
        }

        key = me.getAccessor(entry);

        if (entry.table === true) {

          keys = sol.common.ObjectUtils.isArray(entry.key)
            ? entry.key
            : [entry.key];

          output = keys.map(function (k) {
            return { source: { key: k }, target: { prop: k } };
          });

          me.logger.info(["output {0}, entry={1}", JSON.stringify(output), JSON.stringify(entry)]);

          // convert map table to an array so we can pass user array easily to UserUtils later
          mapTableFormatter = sol.create("sol.common.ObjectFormatter.MapTableToArray", {
            kind: entry.type === "WFMAP" ? "wfMapKeys" : "mapKeys",
            output: output,
            options: {
              propSelector: entry.propSelector || "",
              filter: entry.filter || []
            }
          });

          users = mapTableFormatter.format(me.sord);
          me.logger.info(["users in table {0}", JSON.stringify(users)]);
        } else {
          users = [sol.common.ObjectUtils.getProp(me.sord, key)];
        }

        return users;
      },

      hasAccesssorKey: function (entry) {
        var me = this;
        return me.TEMPLATESORDACCESSORS.hasOwnProperty((entry.type || "").toUpperCase());
      },

      getAccessor: function (entry) {
        var me = this;
        return me.TEMPLATESORDACCESSORS[(entry.type || "").toUpperCase()] + entry.key;
      },

      uniqueList: function (list) {
        var obj = list.reduce(function (acc, item) {
          acc[item] = true;
          return acc;
        }, {});
        return Object.keys(obj);
      }
    });

    /**
     * @member sol.common.ix.functions.ChangeGroup
     * @static
     * @inheritdoc sol.common.ix.FunctionBase#onEnterNode
     */
    function onEnterNode(clInfo, userId, wFDiagram, nodeId) {
      logger.enter("onEnterNode_ChangeGroup", { flowId: wFDiagram.id, nodeId: nodeId });
      var params = sol.common.WfUtils.parseAndCheckParams(wFDiagram, nodeId),
          module;

      sol.common.WfUtils.checkMainAdminWf(wFDiagram);

      params.objId = String(wFDiagram.objId);
      params.flowId = String(wFDiagram.id);
      module = sol.create("sol.common.ix.functions.ChangeGroup", params);

      module.process();

      logger.exit("onEnterNode_ChangeGroup");
    }

    /**
     * @member sol.common.ix.functions.ChangeGroup
     * @static
     * @inheritdoc sol.common.ix.FunctionBase#onExitNode
     */
    function onExitNode(clInfo, userId, wFDiagram, nodeId) {
      logger.enter("onExitNode_ChangeGroup", { flowId: wFDiagram.id, nodeId: nodeId });
      var params = sol.common.WfUtils.parseAndCheckParams(wFDiagram, nodeId),
          module;

      sol.common.WfUtils.checkMainAdminWf(wFDiagram);

      params.objId = wFDiagram.objId;
      params.flowId = String(wFDiagram.id);
      module = sol.create("sol.common.ix.functions.ChangeGroup", params);

      module.process();

      logger.exit("onExitNode_ChangeGroup");
    }

    /**
     * @member sol.common.ix.functions.ChangeGroup
     * @method RF_sol_function_ChangeGroup
     * @static
     * @inheritdoc sol.common.ix.FunctionBase#RF_FunctionName
     */
    function RF_sol_function_ChangeGroup(ec, args) {
      logger.enter("RF_sol_function_ChangeGroup", args);
      var module, result, params = sol.common.ix.RfUtils.parseAndCheckParams(ec, arguments.callee.name, args);

      sol.common.ix.RfUtils.checkMainAdminRights(ec.user, params);
      module = sol.create("sol.common.ix.functions.ChangeGroup", params);

      result = module.process();
      logger.exit("RF_sol_function_ChangeGroup");
      return sol.common.JsonUtils.stringifyQuick(result);
    }