    //@include lib_Class.js

    /**
     * This class provides basic functionality for repository operations.
     *
     * @author ELO Digital Office GmbH
     *
     * @eloas
     * @eloix
     *
     * @requires sol.common.Template
     * @requires sol.common.SordUtils
     * @requires sol.common.AsyncUtils
     * @requires sol.common.FileUtils
     * @requires sol.common.StringUtils
     * @requires sol.common.SordTypeUtils
     *
     */
    sol.define("sol.common.RepoUtils", {
      singleton: true,

      bom: "\uFEFF", // ByteOrderMark (BOM);

      pilcrow: "\u00b6",

      /**
       * Checkout a Sord.
       * @param {String} objId Can be an objId, a GUID or an ARCPATH
       * @param {Object} params (optional)
       * @param {de.elo.ix.client.IXConnection} params.connection (optional) Index server connection
       * @param {de.elo.ix.client.SordZ} [params.sordZ=SordC.mbAllIndex] (optional)
       * @param {de.elo.ix.client.LockZ} [params.lockZ=LockC.NO] (optional)
       * @return {de.elo.ix.client.Sord}
       */
      getSord: function (objId, params) {
        var me = this,
            sordZ, lockZ, conn, sord;

        sordZ = (params && params.sordZ) ? params.sordZ : SordC.mbAllIndex;
        lockZ = (params && params.lockZ) ? params.lockZ : LockC.NO;
        params = params || {};
        conn = params.connection || ixConnect;
        sord = conn.ix().checkoutSord(objId + "", sordZ, lockZ);
        if (me.logger.debugEnabled) {
          me.logger.debug("getSord: sord.id=" + sord.id + ", sord.name=" + sord.name + ", conn.user.id=" + conn.loginResult.user.id +
            ", conn.user.name=" + conn.loginResult.user.name + ", conn.timeZone=" + conn.loginResult.clientInfo.timeZone);
        }
        return sord;
      },

      /**
       * Returns sords by object IDs
       * @param {Array} objIds Object IDs
       * @param {Object} config (optional)
       * @param {de.elo.ix.client.IXConnection} config.connection (optional) Index server connection
       * @param {de.elo.ix.client.SordZ} [config.sordZ=SordC.mbAllIndex] (optional)
       * @param {Boolean} config.keepOrder (optional) Keep the order of the Sords
       * @return {de.elo.ix.client.Sord[]} Sords
       */
      getSords: function (objIds, config) {
        var me = this,
            conn, sordZ, findInfo, findResult, idx, sords, i;
        me.logger.enter("getSords", arguments);

        if (!objIds) {
          throw "Object IDs are empty";
        }
        if (!sol.common.ObjectUtils.isArray(objIds)) {
          throw "Parameter 'objIds' must be an array";
        }

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

        sordZ = config.sordZ || SordC.mbAllIndex;

        findInfo = new FindInfo();
        findInfo.findByIndex = new FindByIndex();
        findInfo.findOptions = new FindOptions();
        findInfo.findOptions.objIds = objIds;

        idx = 0;
        findResult = conn.ix().findFirstSords(findInfo, 100, sordZ);

        sords = [];

        while (true) {
          for (i = 0; i < findResult.sords.length; i++) {
            sords.push(findResult.sords[i]);
          }
          if (!findResult.moreResults) {
            break;
          }
          idx += findResult.sords.length;
          findResult = conn.ix().findNextSords(findResult.searchId, idx, 100, sordZ);
        }
        conn.ix().findClose(findResult.searchId);

        if (config.keepOrder) {
          sords = me.sortSordsByObjIdArray(sords, objIds);
        }

        me.logger.exit("getSords", sords);
        return sords;
      },

      /**
       * Sorts an array of sords by another array of object IDs
       * @param {de.elo.ix.client.Sord[]} sords Sords
       * @param {Array} objIds Object IDs
       * @return {de.elo.ix.client.Sord[]} Sords
       */
      sortSordsByObjIdArray: function (sords, objIds) {
        var me = this,
            sordsObj, resultArr;
        me.logger.enter("sortSordsByObjIdArray", arguments);
        sordsObj = {};
        resultArr = [];

        if (!sords) {
          throw "Sords array is empty";
        }
        if (!objIds) {
          throw "Object ID array is empty";
        }
        sords.forEach(function (sord) {
          sordsObj[String(sord.id)] = sord;
        });
        objIds.forEach(function (objId) {
          var sord;
          sord = sordsObj[String(objId)];
          if (sord) {
            resultArr.push(sord);
          }
        });
        me.logger.exit("sortSordsByObjIdArray", resultArr);
        return resultArr;
      },

      /**
       * Creates a temp file from a repository document with it's element name as file name
       * and downloads the document
       * @param {String} objId Object ID of the repository document.
       * @return {java.io.File} Temporary file.
       */
      createTempFileWithSordName: function (objId) {
        var me = this,
            editInfo, docVersion, url, fileName, tempDir, tempFile;

        me.logger.enter("createTempFileWithSordName", objId);
        editInfo = ixConnect.ix().checkoutDoc(objId + "", null, EditInfoC.mbSordDoc, LockC.NO);
        docVersion = editInfo.document.docs[0];

        if (!docVersion) {
          me.logger.info(["Document version is emtpy: objId={0}", objId]);
          return;
        }

        url = docVersion.url;
        fileName = sol.common.FileUtils.sanitizeFilename(editInfo.sord.name) + "." + editInfo.document.docs[0].ext;
        tempDir = new File(java.lang.System.getProperty("java.io.tmpdir"), "ELO_" + java.lang.System.nanoTime());
        tempDir.mkdir();
        tempFile = new File(tempDir.canonicalPath, fileName);
        ixConnect.download(url, tempFile);

        tempFile.deleteOnExit();
        tempDir.deleteOnExit();

        me.logger.exit("createTempFileWithSordName", tempFile.canonicalPath + "");
        return tempFile;
      },

      /**
       * Finds the children of an element.
       * @param {String} objId
       * @param {Object} config
       * @param {Boolean} config.includeFolders
       * @param {Boolean} config.includeDocuments
       * @param {Boolean} [config.includeReferences=false] (optional)
       * @param {de.elo.ix.client.SordZ} [config.sordZ=SordC.mbAll] (optional) `SordC.mbOnlyId` and `SordC.mbOnlyGuid` are not working
       * @param {Boolean} [config.recursive=false] (optional) If true, subfolders will be included (use carefully)
       * @param {Number} [config.level=3] (optional) If subfolders are included, this restricts the search depth (`-1` for max. depth)
       * @param {String} [config.maskId] (optional) If set, find objects related to this mask ID or name
       * @param {String[]} [config.maskIds] (optional) If set, find objects related to these mask IDs or names
       * @param {de.elo.ix.client.FindOptions} config.findOptions (optional) If set, this `FindOptions` will be applied the the search
       * @param {Object} config.objKeysObj (optional) Find by values
       * @param {String} config.name (optional) Filters the result by the sord name (all elements containing `name`, for exact matches see `exactName`)
       * @param {String} config.ownerId (optional) Filters the result by the owner ID
       * @param {Boolean} [config.exactName=false] (optional) If this is `true`, only objects will be returned, where the name matches exactly `name`
       * @param {de.elo.ix.client.IXConnection} ixConn (optional) This will be used instead of `ìxConnect` (usfull when the search should run in a different user context)
       * @returns {de.elo.ix.client.Sord[]}
       */
      findChildren: function (objId, config, ixConn) {
        var me = this,
            children, findInfo, findChildren, findByType, findByIndex, includeReferences,
            sordZ, recursive, level, objKeys, key, idx, findResult, i;

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

        config = config || {};

        children = [];
        findInfo = new FindInfo();
        findChildren = new FindChildren();
        findByType = new FindByType();
        findByIndex = new FindByIndex();
        includeReferences = config.includeReferences || false;
        sordZ = config.sordZ || SordC.mbAll;
        recursive = config.recursive || false;
        level = config.level || 3;
        objKeys = [];

        ixConn = ixConn || ixConnect;

        me.logger.debug(["findChildren: conn.user.name={0}", ixConn.loginResult.user.name]);

        findChildren.parentId = objId + "";
        findChildren.mainParent = !includeReferences;
        findChildren.endLevel = (recursive) ? level : 1;

        if (config.includeFolders != undefined) {
          findByType.typeStructures = config.includeFolders;
        }
        if (config.includeDocuments != undefined) {
          findByType.typeDocuments = config.includeDocuments;
        }

        if (config.maskId != undefined) {
          findByIndex.maskId = config.maskId;
        }

        if (config.maskIds != undefined) {
          findByIndex.maskIds = config.maskIds;
        }

        if (config.name !== undefined) {
          findByIndex.name = config.name;
          if (config.exactName === true) {
            findByIndex.exactName = true;
          }
        }
        if (config.objKeysObj) {
          for (key in config.objKeysObj) {
            if (config.objKeysObj.hasOwnProperty(key)) {
              objKeys.push(me.createObjKey("", key, config.objKeysObj[key]));
            }
          }
          findByIndex.objKeys = objKeys;
        }

        if (config.ownerId != undefined) {
          findByIndex.ownerId = config.ownerId;
        }

        findInfo.findChildren = findChildren;
        findInfo.findByIndex = findByIndex;

        if (config.includeFolders || config.includeDocuments) {
          findInfo.findByType = findByType;
        }
        if (config.findOptions != undefined) {
          findInfo.findOptions = config.findOptions;
        }

        try {
          idx = 0;
          findResult = ixConn.ix().findFirstSords(findInfo, 1000, sordZ);
          while (true) {
            for (i = 0; i < findResult.sords.length; i++) {
              children.push(findResult.sords[i]);
            }
            if (!findResult.moreResults) {
              break;
            }
            idx += findResult.sords.length;
            findResult = ixConn.ix().findNextSords(findResult.searchId, idx, 1000, sordZ);
          }
        } finally {
          if (findResult) {
            ixConn.ix().findClose(findResult.searchId);
          }
        }
        me.logger.exit("findChildren", children);
        return children;
      },

      /**
       * Returns the first child
       * @param {Object} config Config
       * @param {de.elo.ix.client.Sord} config.parentId Parent Sord ID
       * @param {Boolean} [config.includeDocuments=true] Include documents
       * @param {Boolean} [config.includeFolders=true] Include folders
       * @return {de.elo.ix.client.Sord} First child
       */
      getFirstChild: function (config) {
        var me = this,
            sords, firstChildDocSord;

        config = config || {};
        config.includeDocuments = (typeof config.includeDocuments == "undefined") ? true : config.includeDocuments;
        config.includeFolders = (typeof config.includeFolders == "undefined") ? true : config.includeFolders;

        if (!config.parentId) {
          throw "Parent ID is empty";
        }

        sords = me.findChildren(config.parentId, {
          includeDocuments: config.includeDocuments,
          includeFolders: config.includeFolders
        });

        if (!sords || (sords.length < 1)) {
          return null;
        }

        firstChildDocSord = sords[0];
        return firstChildDocSord;
      },

      /**
       * Finds sords
       * @param {Object} params Parameters
       * @param {Object} params.objKeysObj Map that contains key-value pairs or objects
       *     Example:
       *     {
       *       "objKeysObj": {
       *         "VISITOR_STATUS": "PR*"
       *         "SOL_TYPE": { value: '"VISITOR" OR "VISITOR_GROUP" OR "VISITOR_COMPANY" OR "LONG_TERM_BADGE"', oneTerm: false }
    	 *       }
       *     }
       *     If `oneTerm`is true, then the value is treated as one whole string.
       * @param {String} params.maskId (optional) If set, find objects related to this mask ID or name
       * @param {String[]} params.maskIds (optional) If set, find objects related to these mask IDs or names
       * @param {de.elo.ix.client.SordZ} [params.sordZ=SordC.mbAll] (optional) `SordC.mbOnlyId` and `SordC.mbOnlyGuid` are not working
       * @param {de.elo.ix.client.IXConnection} params.ixConn (optional) This will be used instead of `ìxConnect` (usfull when the search should run in a different user context)
       * @returns {de.elo.ix.client.Sord[]}
       *
       */
      findSords: function (params) {
        var me = this,
            objKeys = [],
            sords = [],
            findInfo, sordZ, key, i, idx, findResult, ixConn, entry, value, oneTerm;

        me.logger.enter("findSords", params);

        params = params || {};

        ixConn = params.ixConn || ixConnect;

        sordZ = params.sordZ || SordC.mbAll;

        findInfo = new FindInfo();

        if (params.objKeysObj || params.maskId || params.maskIds) {
          findInfo.findByIndex = new FindByIndex();

          if (params.objKeysObj) {
            for (key in params.objKeysObj) {
              if (params.objKeysObj.hasOwnProperty(key)) {
                entry = params.objKeysObj[key];
                if (typeof entry === "object") {
                  value = entry.value;
                  oneTerm = entry.oneTerm;
                } else {
                  value = entry + "";
                  oneTerm = (value.trim().indexOf("\"") == 0);
                }

                if (oneTerm) {
                  findInfo.findOptions = new FindOptions();
                  // eslint-disable-next-line no-undef
                  findInfo.findOptions.searchMode = SearchModeC.ONE_TERM;
                }
                objKeys.push(me.createObjKey("", key, value));
              }
            }
            findInfo.findByIndex.objKeys = objKeys;
          }

          if (params.maskId != undefined) {
            findInfo.findByIndex.maskId = params.maskId;
          }

          if (params.maskIds != undefined) {
            findInfo.findByIndex.maskIds = params.maskIds;
          }
        }

        try {
          idx = 0;
          findResult = ixConn.ix().findFirstSords(findInfo, 1000, sordZ);
          while (true) {
            for (i = 0; i < findResult.sords.length; i++) {
              sords.push(findResult.sords[i]);
            }
            if (!findResult.moreResults) {
              break;
            }
            idx += findResult.sords.length;
            findResult = ixConn.ix().findNextSords(findResult.searchId, idx, 1000, sordZ);
          }
        } finally {
          if (findResult) {
            ixConn.ix().findClose(findResult.searchId);
          }
        }
        me.logger.exit("findSords", sords);

        return sords;
      },

      /**
       * Builds a search value string for an OR search
       * @param {Array} values
       * @returns {String}
       */
      buildOrValuesSearchString: function (values) {
        if (!values) {
          return "";
        }
        if (values.length == 1) {
          return values[0];
        }
        return values.map(function (value) {
          return "\"" + value + "\"";
        }).join(" OR ");
      },

      /**
       * Downloads a document from the repository
       * @param {String} objId Object ID of the document
       * @param {Object} config Configuration
       * @param {String} config.dstDirPath (optional) Destination directory path. `config.fileName` must also be set.
       * @param {String} config.fileName (optional) File name
       * @param {String} config.extension (optional) Extention
       * @param {java.io.File} config.file (optional) Destination file
       * @param {Boolean} config.createUniqueFileName (optional) If true the filename will be extended by a number if necessary
       * @return {String} Path of the downloaded file
       */
      downloadToFile: function (objId, config) {
        var me = this,
            editInfo, uniqueFileNamePart, counter,
            file, url;
        me.logger.enter("downloadToFile", arguments);
        config = config || {};
        editInfo = ixConnect.ix().checkoutDoc(objId + "", null, EditInfoC.mbSordDoc, LockC.NO);
        uniqueFileNamePart = "";
        counter = 0;
        if (!editInfo.document.docs || (editInfo.document.docs.length == 0)) {
          me.logger.exit("downloadToFile");
          return;
        }
        url = editInfo.document.docs[0].url;
        config.extension = config.extension || editInfo.document.docs[0].ext;

        do {
          if (counter > 0) {
            uniqueFileNamePart = "_" + sol.common.StringUtils.padLeft(counter, 3);
          }
          if (config.dstDirPath) {
            config.fileName = config.fileName || sol.common.FileUtils.sanitizeFilename(editInfo.sord.name);
            config.filePath = config.dstDirPath + File.separator + config.fileName + uniqueFileNamePart + "." + config.extension;
          }
          file = config.file || new File(config.filePath);
          if (file.exists() && !config.createUniqueFileName) {
            throw "File already exists: " + file.absolutePath;
          }
          counter++;
        } while (file.exists());

        if (config.createDirs) {
          org.apache.commons.io.FileUtils.forceMkdir(file.parentFile);
        }
        ixConnect.download(url, file);
        me.logger.exit("downloadToFile", file.absolutePath + "");
        return file.absolutePath;
      },

      replacementChar: "\uFFFD", // Replacement char

      /**
       * Downloads the content of a repository document into a string
       * @param {String} objId Object ID of the document. If a document version should be loaded, this has to be null
       * @param {String} docId If a docId is supplied, the function will try to download the version only, if objId is null.
       * @param {Object} params (optional) Additional parameter
       * @param {Boolean} [params.preserveBOM=false] (optional) If `true`, the BOM will not be removed (if present)
       * @param {Array} param.charsets=[UTF-8] Charsets, e.g. ["UTF-8", "ISO-8859-1"]
       * @param {de.elo.ix.client.IXConnection} params.connection (optional) Index server connection
       * @return {String} Content as string.
       */
      downloadToString: function (objId, docId, params) {
        var me = this,
            bytes, content, i, charset;

        params = params || {};
        params.charsets = params.charsets || ["UTF-8"];

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

        bytes = me.downloadToByteArray(objId, docId, params);

        for (i = 0; i < params.charsets.length; i++) {
          charset = params.charsets[i];
          content = new java.lang.String(bytes, charset) + "";
          if ((i == params.charsets.length - 1) || (content.indexOf(me.replacementChar) < 0)) {
            break;
          }
        }

        if (params.preserveBOM === true) {
          me.logger.exit("downloadToString", content);
          return content;
        }
        me.logger.exit("downloadToString");
        content = content.replace(me.bom, "");

        return content;
      },

      /**
       * Downloads the content of a repository document into a base64 string
       * @param {String} objId Object ID of the document. If a document version should be loaded, this has to be null
       * @param {String} docId If a docId is supplied, the function will try to download the version only, if objId is null.
       * @return {String} Content as base64 string.
       */
      downloadToBase64String: function (objId, docId) {
        var me = this,
            bytes;
        me.logger.enter("downloadToBase64String", arguments);
        bytes = me.downloadToByteArray(objId, docId);
        me.logger.exit("downloadToBase64String");
        return String(Packages.org.apache.commons.codec.binary.Base64.encodeBase64String(bytes));
      },

      /**
       * Downloads the content of a repository document into a byte array
       * @param  {String} objId Object ID of the document. If a document version should be loaded, this has to be null
       * @param {String} docId If a docId is supplied, the function will try to download the version only, if objId is null.
       * @param {de.elo.ix.client.IXConnection} params (optional)
       * @param {de.elo.ix.client.IXConnection} params.connection (optional) Index server connection
       * @return {java.lang.Byte[]} Content as byte array.
       */
      downloadToByteArray: function (objId, docId, params) {
        var me = this,
            inputStream, bytes;

        params = params || {};

        inputStream = me.downloadToStream(objId, docId, params);
        bytes = Packages.org.apache.commons.io.IOUtils.toByteArray(inputStream);
        inputStream.close();
        return bytes;
      },

      /**
       * @private
       * @param {String} objId
       * @param {String} docId
       * @param {Object} params
       * @param {de.elo.ix.client.IXConnection} params.connection
       * @return {java.io.InputStream}
       */
      downloadToStream: function (objId, docId, params) {
        var me = this,
            url, conn, inputStream;

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

        url = me.getDownloadUrl(objId, docId, params);

        me.logger.debug(["downloadToStream: conn.user.name={0}", conn.loginResult.user.name]);

        inputStream = conn.download(url, 0, -1);

        return inputStream;
      },

      /**
       * @private
       * @param {String} objId
       * @param {String} docId
       * @param {Object} params
       * @param {de.elo.ix.client.IXConnection} params.connection
       * @return {String}
       */
      getDownloadUrl: function (objId, docId, params) {
        var me = this,
            editInfo, docs, conn, url;

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

        if (!objId && !docId) {
          throw "objId and docId are both empty";
        }

        me.logger.debug(["getDownloadUrl: conn.user.name={0}", conn.loginResult.user.name]);

        if (objId) {
          editInfo = conn.ix().checkoutDoc(objId + "", null, EditInfoC.mbSordDoc, LockC.NO);
        } else if (docId) {
          editInfo = conn.ix().checkoutDoc(null, docId + "", EditInfoC.mbDocument, LockC.NO);
        }
        docs = editInfo.document.docs;
        if (!docs || (docs.length == 0)) {
          throw "There are no documents";
        }
        url = String(docs[0].url);

        return url;
      },

      contentTypeExtensions: {
        bmp: "image/bmp",
        ico: "image/x-ico",
        jpg: "image/jpeg",
        png: "image/png"
      },

      /**
       * @private
       * @param {String} objId
       * @param {String} docId
       * @param {Object} config Configuration
       * @param {String} config.extension Extension
       * @return {java.io.InputStream}
       */
      downloadToFileData: function (objId, docId, config) {
        var me = this,
            inputStream, fileData;
        me.logger.enter("downloadToFileData", arguments);
        config = config || {};
        inputStream = me.downloadToStream(objId, docId);
        fileData = new FileData();
        fileData.contentType = me.contentTypeExtensions[config.extension] || "application/octet-stream";
        fileData.data = Packages.org.apache.commons.io.IOUtils.toByteArray(inputStream);
        inputStream.close();
        me.logger.exit("downloadToFileData");
        return fileData;
      },

      /**
       * Downloads the content of a small repository document into a string
       * @param {String} objId Object ID of the document. If a document version should be loaded, this has to be null
       * @param {String} docId If a docId is supplied, the function will try to download the version only, if objId is null.
       * @return {java.lang.String} Content as string.
       */
      downloadSmallContentToString: function (objId, docId) {
        var me = this,
            content = "",
            editInfo;

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

        if (objId) {
          editInfo = ixConnect.ix().checkoutSord(objId, new EditInfoZ(0, new SordZ(SordC.mbSmallDocumentContent)), LockC.NO);
          if (editInfo.sord.docVersion) {
            content = new java.lang.String(editInfo.sord.docVersion.fileData.data, "UTF-8");
          } else {
            me.logger.warn(["downloadSmallContentToString: No document version: objId={0}, docId={1}", objId || "", docId || ""]);
          }
        } else {
          editInfo = ixConnect.ix().checkoutDoc(null, docId + "", EditInfoC.mbSordDocSmallContent, LockC.NO);
          content = new java.lang.String(editInfo.document.docs[0].fileData.data, "UTF-8");
        }

        content = content.replace(me.bom, "");
        me.logger.exit("downloadSmallContentToString");

        return content;
      },

      /**
       * Uploads the content of a small repository document
       * @param {String} objId Object ID of the document.
       * @param {String} content Content as string.
       * @param {Object} config Configuration
       * @param {de.elo.ix.client.IXConnection} config.connection Index server connection
       */
      uploadSmallContent: function (objId, content, config) {
        var me = this,
            conn, fileContent, editInfo, sordZ;

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

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

        sordZ = new SordZ(SordC.mbSmallDocumentContent);

        fileContent = new java.lang.String(content);
        editInfo = conn.ix().checkoutSord(objId, new EditInfoZ(0, sordZ), LockC.NO);
        editInfo.document = new Packages.de.elo.ix.client.Document();
        editInfo.document.docs = [new DocVersion()];
        editInfo.document.docs[0].workVersion = true;
        editInfo.document.docs[0].ext = editInfo.sord.docVersion.ext;
        editInfo.document.docs[0].contentType = editInfo.sord.docVersion.contentType;
        editInfo.document.docs[0].fileData = new FileData();
        editInfo.document.docs[0].fileData.data = fileContent.getBytes(java.nio.charset.Charset.forName("UTF-8"));
        conn.ix().checkinDocEnd(editInfo.sord, sordZ, editInfo.document, LockC.NO);
        me.logger.exit("uploadSmallContent");
      },

      /**
       * Creates a new repository document or saves a new version to an existing document.
       * @param {Object} saveToRepoConfig
       * @param {String} saveToRepoConfig.name Name
       * @param {String} saveToRepoConfig.objId Object which should be updated (`parentId`, `repoPath` and `tryUpdate` are redundant in this case); objId will be used first
       * @param {String} saveToRepoConfig.parentId Parent folder object ID
       * @param {String} saveToRepoConfig.repoPath Complete destination repository path
       * @param {String} saveToRepoConfig.maskId Mask ID
       * @param {Object} saveToRepoConfig.objKeysObj Map that contains key-value pairs
       * @param {java.io.File} saveToRepoConfig.file File
       * @param {String} saveToRepoConfig.extension
       * @param {String} saveToRepoConfig.contentString String to save
       * @param {String} saveToRepoConfig.withoutBom Saves a string without BOM
       * @param {Object} saveToRepoConfig.contentObject Object to save
       * @param {java.io.OutputStream} saveToRepoConfig.outputStream Output stream to save
       * @param {String} saveToRepoConfig.base64Content Base64 encoded content to save
       * @param {Boolean} saveToRepoConfig.tryUpdate Inserts a new version if the object already exists
       * @param {String} saveToRepoConfig.version Version
       * @param {String|Number} saveToRepoConfig.versionIncrement Version increment, i.g. `1`
       * @param {String} saveToRepoConfig.versionComment Version comment
       * @param {String} saveToRepoConfig.ownerId Owner Id for a new version if the object already exists
       * @param {de.elo.ix.client.IXConnection} saveToRepoConfig.connection Index server connection
       * @param {Number} saveToRepoConfig.encryptionSet Encryption set
       * @return {String} Object ID
       */
      saveToRepo: function (saveToRepoConfig) {
        var me = this,
            parentRepoPath, bytes, inputStream, editInfo, objKeys, key, objId, conn,
            newVersionString = "",
            currentVersionString, encryptionSet;

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

        conn = saveToRepoConfig.connection || ixConnect;

        if (saveToRepoConfig.repoPath) {
          saveToRepoConfig.name = me.getNameFromPath(saveToRepoConfig.repoPath);
          parentRepoPath = me.getParentPath(saveToRepoConfig.repoPath);
          saveToRepoConfig.parentId = me.getObjId(parentRepoPath);
        }

        saveToRepoConfig.objKeysObj = saveToRepoConfig.objKeysObj || {};

        saveToRepoConfig.maskId = saveToRepoConfig.maskId || "";

        if (saveToRepoConfig.objId || (saveToRepoConfig.tryUpdate && saveToRepoConfig.repoPath)) {
          try {
            objId = saveToRepoConfig.objId || me.getObjId(saveToRepoConfig.repoPath);
            if (objId) {
              editInfo = conn.ix().checkoutDoc(objId + "", null, EditInfoC.mbSordDoc, LockC.NO);
            }
          } catch (ignore) {
            // Object not found
          }
        }

        if (editInfo && !saveToRepoConfig.name) {
          saveToRepoConfig.name = editInfo.sord.name;
        }

        if (!editInfo) {
          editInfo = conn.ix().createDoc(saveToRepoConfig.parentId + "", saveToRepoConfig.maskId + "", null, EditInfoC.mbSordDocAtt);
          objKeys = Array.prototype.slice.call(editInfo.sord.objKeys);
          objKeys.push(me.createObjKey(DocMaskLineC.ID_FILENAME, DocMaskLineC.NAME_FILENAME, ""));
          editInfo.sord.objKeys = objKeys;

          if (saveToRepoConfig.ownerId) {
            editInfo.sord.ownerId = saveToRepoConfig.ownerId;
          }
        }

        if (saveToRepoConfig.base64Content) {
          bytes = Packages.org.apache.commons.codec.binary.Base64.decodeBase64(saveToRepoConfig.base64Content);
        }

        if (saveToRepoConfig.outputStream) {
          bytes = saveToRepoConfig.outputStream.toByteArray();
          saveToRepoConfig.outputStream.close();
        }

        if (saveToRepoConfig.contentObject) {
          saveToRepoConfig.contentString = JSON.stringify(saveToRepoConfig.contentObject, null, 2);
          saveToRepoConfig.extension = saveToRepoConfig.extension || "json";
        }

        if (saveToRepoConfig.contentString) {
          if (!saveToRepoConfig.withoutBom) {
            saveToRepoConfig.contentString = me.bom + saveToRepoConfig.contentString;
          }
          bytes = new java.lang.String(saveToRepoConfig.contentString).getBytes("UTF-8");
        }

        if (saveToRepoConfig.file) {
          saveToRepoConfig.fileName = saveToRepoConfig.file.name;
          if (!saveToRepoConfig.name) {
            saveToRepoConfig.name = Packages.org.apache.commons.io.FilenameUtils.removeExtension(saveToRepoConfig.file.name);
          }
          if (!saveToRepoConfig.extension) {
            saveToRepoConfig.extension = Packages.org.apache.commons.io.FilenameUtils.getExtension(saveToRepoConfig.file.absolutePath);
          }
        } else if (bytes) {
          saveToRepoConfig.fileName = saveToRepoConfig.name + "." + saveToRepoConfig.extension;
          if (sol.common.FileUtils) { // check to avoid errors in older solution versions without proper include
            saveToRepoConfig.fileName = sol.common.FileUtils.sanitizeFilename(saveToRepoConfig.fileName);
          } else {
            me.logger.debug(["Could not sanitize file name ('{0}'): missing include of 'sol.common.FileUtils'", saveToRepoConfig.fileName]);
          }
        }
        editInfo.sord.name = saveToRepoConfig.name;

        for (key in saveToRepoConfig.objKeysObj) {
          if (saveToRepoConfig.objKeysObj.hasOwnProperty(key) && key) {
            sol.common.SordUtils.setObjKeyValue(editInfo.sord, key, saveToRepoConfig.objKeysObj[key]);
          }
        }

        if (saveToRepoConfig.fileName) {
          sol.common.SordUtils.setObjKeyValue(editInfo.sord, DocMaskLineC.NAME_FILENAME, saveToRepoConfig.fileName);
        }

        saveToRepoConfig.versionIncrement = saveToRepoConfig.versionIncrement || 1;
        if (saveToRepoConfig.versionIncrement && editInfo.document.docs && (editInfo.document.docs.length > 0)) {
          currentVersionString = editInfo.document.docs[0].version + "";
        } else {
          newVersionString = (saveToRepoConfig.versionIncrement || "") + "";
        }

        editInfo.document.docs = [new DocVersion()];

        if (saveToRepoConfig.versionIncrement) {
          if (currentVersionString) {
            try {
              newVersionString = me.calcNextVersion(objId, saveToRepoConfig.versionIncrement);
            } catch (ignore) {}
          }
        }

        if (saveToRepoConfig.version) {
          newVersionString = saveToRepoConfig.version;
        }

        if (newVersionString) {
          editInfo.document.docs[0].version = newVersionString;
        }

        if (saveToRepoConfig.versionComment) {
          editInfo.document.docs[0].comment = saveToRepoConfig.versionComment;
        }

        if (saveToRepoConfig.ownerId) {
          editInfo.document.docs[0].ownerId = saveToRepoConfig.ownerId;
        }

        editInfo.document.docs[0].ext = saveToRepoConfig.extension;
        editInfo.document.docs[0].pathId = editInfo.sord.path;

        encryptionSet = (typeof saveToRepoConfig.encryptionSet != "undefined") ? saveToRepoConfig.encryptionSet : editInfo.sord.details.encryptionSet;
        editInfo.sord.details.encryptionSet = encryptionSet;
        editInfo.document.docs[0].encryptionSet = encryptionSet;

        editInfo.document = conn.ix().checkinDocBegin(editInfo.document);

        if (saveToRepoConfig.file) {
          editInfo.document.docs[0].uploadResult = conn.upload(editInfo.document.docs[0].url, saveToRepoConfig.file);
        } else if (bytes) {
          inputStream = new java.io.ByteArrayInputStream(bytes);
          editInfo.document.docs[0].uploadResult = conn.upload(editInfo.document.docs[0].url, inputStream, bytes.length, "application/octet-stream");
          inputStream.close();
        } else {
          throw "Input data is missing.";
        }

        editInfo.document = conn.ix().checkinDocEnd(editInfo.sord, SordC.mbAll, editInfo.document, LockC.NO);
        objId = editInfo.document.objId;

        me.logger.debug("Document saved to repository: objId=" + objId);
        me.logger.exit("saveToRepo");
        return String(objId);
      },

      /**
       * @private
       * Creates an ObjKey object
       * @param {String} id ID of the ObjKey
       * @param {String} name Name of the ObjKey
       * @param {String} value
       * @return {de.elo.ix.client.ObjKey} Created ObjKey
       */
      createObjKey: function (id, name, value) {
        var objKey = new ObjKey();
        if (id) {
          objKey.id = id;
        }
        objKey.name = name;
        objKey.data = [value];
        return objKey;
      },

      /**
       * Exports a repository folder into an ELO ZIP file
       * @param {java.io.File} exportZipFile
       * @param {Object} exportOptions Export options, see de.elo.ix.client.ExportExtOptions
       */
      exportRepoData: function (exportZipFile, exportOptions) {
        var me = this,
            exportExtOptions, prop, exportId, exportZipUrl;
        me.logger.enter("exportRepoData", arguments);
        if (!exportOptions) {
          throw "Export options are missing.";
        }
        if (exportOptions.srcList) {
          exportOptions.srcList.forEach(function (objId) {
            ixConnect.ix().checkoutSord(objId, SordC.mbOnlyId, LockC.NO);
          });
        }
        exportExtOptions = new ExportExtOptions();
        for (prop in exportOptions) {
          if (exportOptions.hasOwnProperty(prop)) {
            exportExtOptions[prop] = exportOptions[prop];
          }
        }
        exportId = ixConnect.ix().startExportExt(exportExtOptions);
        sol.common.AsyncUtils.waitForJob(exportId);
        exportZipUrl = ixConnect.ix().getExportZipUrl(exportId);
        ixConnect.download(exportZipUrl, exportZipFile);
        ixConnect.ix().finishExport(exportId);
        me.logger.exit("exportRepoData");
      },

      /**
       * Imports a ELO ZIP file into the repository
       * @param {java.io.File} importZipFile
       * @param {String} dstRepoPath Destination repository path
       * @param {Number} guidMethod GUID method, see de.elo.ix.client.ImportOptionsC
       * @param {Number} options Import options, see de.elo.ix.client.ImportOptionsC
       */
      importRepoData: function (importZipFile, dstRepoPath, guidMethod, options) {
        var me = this,
            importId, importZipUrl, dstObjId;
        me.logger.enter("importRepoData", arguments);
        if (typeof guidMethod === "undefined") {
          guidMethod = ImportOptionsC.GUIDS_KEEP;
        }
        if (typeof options === "undefined") {
          options = 0;
        }
        if (dstRepoPath) {
          dstObjId = me.preparePath(dstRepoPath);
        } else {
          options |= ImportOptionsC.USE_EXPORTED_PATH;
        }
        importId = ixConnect.ix().startImport(dstObjId + "", guidMethod, options);
        importZipUrl = ixConnect.ix().getImportZipUrl(importId);
        ixConnect.upload(importZipUrl, importZipFile);
        sol.common.AsyncUtils.waitForJob(importId);
        me.logger.exit("importRepoData");
      },

      /**
       * Returns the object ID of a given repository path
       * @param {String} path Repository path. The path separator is defined by the first character or the first charcter after "ARCPATH:"
       * @param {Object} params Parameters
       * @param {Boolean} params.resolveGuid Resolve GUID
       * @return {String} The ID of the new element, or null if it does not exist
       */
      getObjId: function (path, params) {
        var me = this,
            conn, sord, objId;

        params = params || {};
        me.logger.enter("getObjId", { path: path, params: params });

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

        if (me.isObjId(path)) {
          me.logger.exit("getObjId", { objId: path });
          return path;
        }

        if (me.isGuid(path)) {
          if (params.resolveGuid) {
            try {
              sord = conn.ix().checkoutSord(path + "", SordC.mbOnlyId, LockC.NO);
              objId = sord.id + "";
              me.logger.exit("getObjId", { objId: objId });
              return objId;
            } catch (ex) {
              me.logger.warn(["Can't find GUID: guid={0}", path]);
              return;
            }
          } else {
            me.logger.exit("getObjId", { guid: path });
            return path;
          }
        }

        path = me.normalizePath(path, true);

        try {
          sord = conn.ix().checkoutSord(path + "", SordC.mbOnlyId, LockC.NO);
          objId = sord.id + "";
          me.logger.exit("getObjId", { objId: objId });
          return objId;
        } catch (ignore) {
          // Object not found
        }

        me.logger.exit("getObjId");
      },

      /**
       * Returns the object GUID of a given Object ID
       * @param {String} objId Object ID
       * @return {String} GUID
       */
      getGuid: function (objId) {
        var me = this,
            conn, sord;
        me.logger.enter("getGuid", arguments);
        conn = (typeof ixConnectAdmin !== "undefined") ? ixConnectAdmin : ixConnect;
        try {
          sord = conn.ix().checkoutSord(objId, SordC.mbOnlyGuid, LockC.NO);
          me.logger.exit("getGuid", sord.guid);
          return sord.guid;
        } catch (ignore) {
          // Object not found
        }
        me.logger.exit("getGuid");
      },

      /**
       * Checks wether a path exists
       * @param {String} repoPath Repository path
       * @return {Boolean}
       */
      exists: function (repoPath) {
        var me = this;
        return !!me.getObjId(repoPath);
      },

      /**
       * Returns true if the given string is an object ID
       * @param {String} str Input string
       * @return {Boolean}
       */
      isObjId: function (str) {
        return /^[\d]{1,20}$/.test(String(str));
      },

      /**
       * Returns true if the given string is an object ID
       * @param {String} str Input string
       * @return {Boolean}
       */
      isGuid: function (str) {
        return /^\(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\)$/.test(String(str));
      },

      /**
       * Returns true if the given string is an arcpath
       * @param {String} str Input string
       * @return {Boolean}
       */
      isArcpath: function (str) {
        return (str || "").indexOf("ARCPATH") === 0;
      },

      /**
       * Returns true if the given string is an okay path
       * @param {String} str Input string
       * @return {Boolean}
       */
      isOkeyPath: function (str) {
        return (str || "").indexOf("OKEY") === 0;
      },

      /**
       * Returns true if the given string is an md5 hash path
       * @param (String) str Input string
       * @returns {Boolean}
       */
      isMd5HashPath: function(str) {
        return (str || "").indexOf("MD5") === 0;
      },

      isLMatchPath: function(str) {
        return (str || "").indexOf("LMATCH") === 0;
      },

      /**
       * Returns true if str is a possible elo object identifier.
       *
       * Check `checkoutSord` documentation to see all valid object identifiers
       *
       * @param {String} str Input string
       * @return {Boolean}
       */
      isRepoId: function (str) {
        var me = this;
        return me.isObjId(str)
          || me.isGuid(str)
          || me.isArcpath(str)
          || me.isOkeyPath(str)
          || me.isMd5HashPath(str)
          || me.isLMatchPath(str);
      },

      /**
       * Looks up an objId by an index field value.
       *
       *     sol.common.RepoUtils.getObjIdByIndex( { mask: "Invoice", objKeyData: [ { key: "INVOICE_ID", value: "12345" } ] } );
       *
       * @param {Object} filter
       * @param {String} filter.mask (optional) Additional limit search by mask
       * @param {Object[]} filter.objKeyData Objects with key and value for the lookup
       * @returns {String} The objId
       * @throws Throws an exception, if result is not unique
       * @throws Throws an exception, if there is no result
       */
      getObjIdByIndex: function (filter) {
        var me = this,
            findInfo, objKeys,
            findResult, ids, _result, i, sord;
        me.logger.enter("getObjIdByIndex", arguments);
        findInfo = new FindInfo();
        objKeys = [];

        if (!filter || !filter.objKeyData || !Array.isArray(filter.objKeyData)) {
          throw "illegal filter: 'objKeyData' cannot be undefined and has to be an Array ";
        }

        filter.objKeyData.forEach(function (data) {
          if (data.key && data.value) {
            objKeys.push(me.createObjKey(null, data.key, data.value));
          }
        });

        findInfo.findByIndex = new FindByIndex();
        findInfo.findByIndex.objKeys = objKeys;

        if (filter.mask) {
          findInfo.findByIndex.maskId = filter.mask;
        }

        findResult = ixConnect.ix().findFirstSords(findInfo, 2, SordC.mbOnlyId);
        if (findResult.ids) { // Expected result when searching with SordC.mbOnlyId
          ids = findResult.ids;
        } else if (findResult.sords) { // in some cases the results will be returned this way, regardless of the SordC.mbOnlyId selector
          ids = [];
          for (i = 0; i < findResult.sords.length; i++) {
            sord = findResult.sords[i];
            ids.push(sord.id);
          }
        }
        if (!ids || ids.length <= 0) {
          throw "no element found";
        }
        if (ids.length > 1) {
          throw "no unique result";
        }
        _result = ids[0];
        me.logger.exit("getObjIdByIndex", _result);
        return _result;
      },

      /**
       * Returns the object ID of an object which is defined by a start object and an additional relative path
       *
       *    var objId = sol.common.RepoUtils.getObjIdFromRelativePath(123, "/.eloinst");
       *    var objId = sol.common.RepoUtils.getObjIdFromath("ARCPATH:/Administration", "/common/Configuration");
       *
       * If the start folder is adynamic register, this method handels the request a little different:
       *
       * - it determines all children
       * - it searches for a child element with the name specified by 'ath'
       * - unlike the same call on a normal folder, this does not support nested paths (i.e. `ath` can just have one level)
       *
       * @param {String} startFolderId
       * @param {String} relativePath Should start with a separator
       * @returns {String}
       */
      getObjIdFromRelativePath: function (startFolderId, relativePath) {
        var me = this,
            startObjId, startSord, children, objId, _result;

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

        if (!startFolderId) {
          throw "Start folder ID is empty";
        }

        startObjId = me.getObjId(startFolderId);

        if (!relativePath) {
          return startObjId;
        }

        startSord = ixConnect.ix().checkoutSord(startObjId, SordC.mbAllIndex, LockC.NO);

        if (sol.common.SordUtils.isDynamicFolder(startSord)) {
          relativePath = relativePath.substring(1, relativePath.length);
          children = me.findChildren(startSord.id, {
            includeFolders: true,
            includeDocuments: true,
            includeReferences: true
          });
          children.some(function (child) {
            if (child.name == relativePath) {
              objId = child.id;
              return true;
            }
          });
          me.logger.exit("getObjIdFromRelativePath", objId);
          return objId;
        } else {
          _result = ixConnect.ix().checkoutSord("ARCPATH[" + startSord.id + "]:" + relativePath, SordC.mbOnlyId, LockC.NO).id;
          me.logger.exit("getObjIdFromRelativePath", _result);
          return _result;
        }
      },

      /**
       * Returns the object ID of an object which is defined by a relative solution path.
       * Searches in the folders which are defined in /common/Configuration/base.config in baseMergePaths.
       * Starts searching from behind.
       *
       * @param {String} relativePath
       * @returns {String}
       */
      getObjIdFromRelativeSolutionPath: function (relativePath) {
        var me = this,
            commonBaseConfig, baseMergePaths, i;

        commonBaseConfig = sol.create("sol.common.Config", { compose: "/common/Configuration/base.config" }).config;

        baseMergePaths = sol.common.ObjectUtils.clone(commonBaseConfig.baseMergePaths).reverse(); //we have to reverse the array to start searching in the custom part

        for (i = 0; i < baseMergePaths.length; i++) {
          try {
            return me.getObjIdFromRelativePath("ARCPATH:/Administration/" + baseMergePaths[i], relativePath);
          } catch (e) {
            //ignore
          }
        }
      },

      /**
       * Returns repository path of a Sord object
       * @param {de.elo.ix.client.Sord} sord
       * @param {Boolean} withPrefix If true the ARCPATH: prefix will be added.
       * @param {Object} config (optional)
       * @param {String} [config.separator="/"] (optional)
       * @return {String} Repository path
       */
      getPath: function (sord, withPrefix, config) {
        var me = this,
            repoPathParts = [],
            separator, prefix, i, idNames, _result;
        me.logger.enter("getPath", arguments);
        if (!sord || !sord.refPaths || !sord.refPaths[0]) {
          me.logger.exit("getPath", "");
          return "";
        }

        idNames = sord.refPaths[0].path;
        for (i = 0; i < idNames.length; i++) {
          repoPathParts.push(idNames[i].name + "");
        }
        repoPathParts.push(sord.name + "");
        prefix = withPrefix ? "ARCPATH:" : "";
        separator = (config && config.separator) ? config.separator : "/";
        _result = prefix + separator + repoPathParts.join(separator);
        me.logger.exit("getPath", _result);
        return _result;
      },

      /**
       * Returns repository path of an object ID
       * @param {String} objId Object ID
       * @param {Object} config (optional)
       * @param {String} [config.separator="/"] (optional)
       * @return {String} Repository path
       */
      getPathFromObjId: function (objId, config) {
        var me = this,
            conn, sord, path;

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

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

        try {
          sord = conn.ix().checkoutSord(objId, new SordZ(SordC.mbRefPaths), LockC.NO);
        } catch (ignore) {
          //ignore
        }
        if (sord) {
          path = me.getPath(sord, false, config);
          me.logger.exit("getPathFromObjId", path);
          return path;
        }
        me.logger.exit("getPathFromObjId", "");
        return "";
      },

      /**
       * Checks and creates a repository path.
       *
       * The `repoPath` can be in handlebars [handlebars](http://handlebarsjs.com/) syntax and is applied via {@link sol.common.Template}.
       *
       * @param {String} repoPath Repository path. The path separator is defined by the first character or the first charcter after "ARCPATH:"
       * @param {Object} params (optional)
       * @param {String} params.mask (optional) If set, newly created parts of the path get that mask
       * @param {Boolean} params.returnDetails (optional) If set, the created objId will be returned as an object property. If the path already existed, an additional property existed:true will be returned.
       * @param {Boolean} params.skipIfNotExists (optional) If set and the repoPath does not exist, it will not be created. { objId: null, existed: false, skipped: true } will be returned.
       * @param {Object|de.elo.ix.client.Sord} params.data (optional) If set, this is applied to the repoPath, while the repoPath has to be in `handlebars` syntax
       * @param {String|Number} params.sordType (optional) Name or ID of a sord type which will be set on all new elements
       * @return {String|Object} The ID of the new element, or null if something went wrong
       */
      preparePath: function (repoPath, params) {
        var me = this,
            objId, _result,
            returnDetails = !!((params && params.returnDetails === true)),
            skipIfNotExists = !!(params && (params.skipIfNotExists === true));
        me.logger.enter("preparePath", arguments);

        if (params && params.data) {
          if (params.data instanceof Sord) {
            params.data = sol.common.SordUtils.getTemplateSord(params.data);
          }
          repoPath = sol.create("sol.common.Template", { source: repoPath }).apply(params.data);
        }

        objId = me.getObjId(repoPath);

        if (objId) {
          _result = { objId: objId, existed: true };
        } else if (skipIfNotExists) {
          me.logger.info("skipped", objId);
          _result = { objId: null, existed: false, skipped: true };
        } else {
          _result = { objId: me.createPath(repoPath, params), existed: false };
        }

        if (!returnDetails) {
          _result = _result.objId;
        } else {
          _result.path = repoPath;
        }

        me.logger.exit("preparePath", _result);

        return _result;
      },

      /**
       * Checks wether the repository path contains an empty path part
       * @private
       * @param {String} repoPath Repository path
       * @throws {Exception}
       */
      checkRepoPath: function (repoPath) {
        var me = this,
            separator, illegalString;

        if (!repoPath) {
          throw "Repository path is empty";
        }

        separator = me.getPathSeparator(repoPath);
        illegalString = separator + separator;
        if (repoPath.indexOf(illegalString) > -1) {
          throw "Repository path must not contain an empty path part: " + repoPath;
        }
      },

      /**
       * Creates a repository path.
       *
       * If the path contains dynamic content, use {@link #preparePath} instead.
       *
       * @param {String} repoPath A path. The path separator is defined by the first character or the first character after "ARCPATH:"
       * @param {Object} params (optional)
       * @param {String} params.mask (optional) If set, newly created parts of the path get that mask
       * @param {Object} params.rightsConfig Rights configuration
       * @param {String|Number} params.sordType Name or ID of a sord type which will be set on all new elements
       * @return {String} The ID of the new element, or null if something went wrong
       */
      createPath: function (repoPath, params) {
        var me = this,
            delim, sordNames, sords, ids, parentIdMatch, parentId, aclItemInherit, aclItems, fixedSordType, dynSordType,
            accessCode, userAcls, conn;
        me.logger.enter("createPath", arguments);
        conn = (typeof ixConnectAdmin !== "undefined") ? ixConnectAdmin : ixConnect;
        params = params || {};
        parentIdMatch = repoPath.match(/^ARCPATH\[([^\]]+)\]/);
        parentId = parentIdMatch ? parentIdMatch[1] : "1";
        repoPath = me.normalizePath(repoPath);
        delim = me.getPathSeparator(repoPath);
        me.checkRepoPath(repoPath);
        sordNames = repoPath.substring(1).split(delim);
        sords = [];

        params.mask = params.mask || "";

        aclItemInherit = new AclItem();
        aclItemInherit.type = AclItemC.TYPE_INHERIT;
        aclItems = [aclItemInherit];

        if (params.rightsConfig && params.rightsConfig.users) {
          if (params.rightsConfig.rights) {
            accessCode = sol.common.AclUtils.createAccessCode(params.rightsConfig.rights);
          }
          if ((params.rightsConfig.mode == "SET") || (params.rightsConfig.mode == "REPLACE")) {
            aclItems = [];
          }
          userAcls = sol.common.AclUtils.retrieveUserAcl(params.rightsConfig.users, accessCode);
          if (userAcls) {
            userAcls.forEach(function (userAcl) {
              aclItems.push(userAcl);
            });
          }
        }

        try {
          fixedSordType = (params && params.sordType) ? ((typeof params.sordType !== "number") ? sol.common.SordTypeUtils.getSordTypeId(params.sordType) : params.sordType) : null;
          sordNames.forEach(function (name) {
            var sord = conn.ix().createSord(parentId + "", params.mask, SordC.mbAll);
            dynSordType = (!dynSordType) ? ((sord.type <= 6) ? sord.type : 6) : ((dynSordType <= 6) ? dynSordType : 6);
            sord.name = name;
            sord.aclItems = aclItems;
            sord.type = fixedSordType || dynSordType;
            sords.push(sord);
            dynSordType++;
          });

          ids = conn.ix().checkinSordPath(parentId + "", sords, SordC.mbAll);
          me.logger.exit("createPath", ids[ids.length - 1]);
          return ids[ids.length - 1];

        } catch (e) {
          this.logger.error("error creating archive path", e);
        }
        me.logger.exit("createPath", null);
        return null;
      },

      /**
       * Returns the repository path separator character
       * @param {String} repoPath Repository path
       * @return {String} Repository path separator
       */
      getPathSeparator: function (repoPath) {
        var me = this,
            matches, _result;
        me.logger.enter("getPathSeparator", arguments);
        repoPath = me.normalizePath(repoPath);
        if (repoPath && (repoPath.length > 0)) {
          if (repoPath.indexOf("{") == 0) {
            matches = repoPath.match("(?:{+)(?:[^}]+)(?:}+)(.)");
            if (matches && (matches.length == 2)) {
              me.logger.exit("getPathSeparator", matches[1]);
              return matches[1];
            }
          } else {
            _result = String(repoPath).substring(0, 1);
            me.logger.exit("getPathSeparator", _result);
            return _result;
          }
        }
        me.logger.exit("getPathSeparator", "/");
        return "/";
      },

      /**
       * Changes the path separator
       * @param {String} path Repository path
       * @param {String} newSeparator New separator
       * @return {String} Repository path that contains the new separator
       */
      changePathSeparator: function (path, newSeparator) {
        var me = this,
            separator, _result;
        me.logger.enter("changePathSeparator", arguments);
        path = String(path);
        separator = me.getPathSeparator(path);
        newSeparator = newSeparator || me.pilcrow;
        _result = sol.common.StringUtils.replaceAll(path, separator, newSeparator);
        me.logger.exit("changePathSeparator", _result);
        return _result;
      },

      /**
       * Returns the name part of a repository path
       * @param {String} repoPath Repository path
       * @return {String} Name part of the repository path
       */
      getNameFromPath: function (repoPath) {
        var me = this,
            separator, _result;
        me.logger.enter("getNameFromPath", arguments);
        repoPath = me.normalizePath(repoPath);
        separator = me.getPathSeparator(repoPath);
        _result = String(repoPath).split(separator).pop();
        me.logger.exit("getNameFromPath", _result);
        return _result;
      },

      /**
       * Returns the parent repository path
       * @param {String} repoPath Repository path
       * @return {String} Repository path of the parent folder
       */
      getParentPath: function (repoPath) {
        var me = this,
            separator, _result;
        me.logger.enter("getParentPath", arguments);
        separator = me.getPathSeparator(repoPath);
        _result = String(repoPath).substring(0, repoPath.lastIndexOf(separator));
        me.logger.exit("getParentPath", _result);
        return _result;
      },

      /**
       * Normalizes a repository path
       * @param {String} repoPath Repository path
       * @param {Boolean} withPrefix If true the ARCPATH: prefix will be added.
       * @return {String} Normalized repository path
       */
      normalizePath: function (repoPath, withPrefix) {
        repoPath = String(repoPath);
        if (repoPath.indexOf("ARCPATH") == 0) {
          if (withPrefix) {
            return repoPath;
          } else {
            return repoPath.replace(/^ARCPATH[^:]*:/, "");
          }
        } else {
          if (withPrefix) {
            return "ARCPATH:" + repoPath;
          } else {
            return repoPath;
          }
        }
      },

      /**
       * Deletes all references
       * @param {de.elo.ix.client.Sord} sord
       * @return {Array} Reference parent IDs
       */
      deleteAllReferences: function (sord) {
        var me = this,
            i, refPath, objId, refParentId,
            refParentIds = [],
            deleteOptions;

        if (!sord) {
          throw "Sord is empty";
        }
        if (!sord.refPaths) {
          throw "Reference paths are empty";
        }
        if (sord.refPaths.length <= 1) {
          return;
        }

        deleteOptions = new DeleteOptions();

        for (i = 1; i < sord.refPaths.length; i++) {
          refPath = sord.refPaths[i];
          if (refPath.path.length > 0) {
            refParentId = String(refPath.path[refPath.path.length - 1].id);
          } else {
            refParentId = "1";
          }
          refParentIds.push(refParentId);
          objId = sord.id + "";
          try {
            ixConnect.ix().deleteSord(refParentId, objId, LockC.NO, deleteOptions);
          } catch (ex) {
            me.logger.warn(["Cannot delete reference: refParentId={0}, objId={1}", refParentId, objId]);
          }
        }

        return refParentIds;
      },

      /**
       * Deletes a sord
       * @param {String} objId Object ID
       * @param {Object} config Configuration
       * @param {Object} config.parentId parentId Parent ID
       * @param {Boolean} config.deleteFinally
       * @param {Boolean} config.silent
       */
      deleteSord: function (objId, config) {
        var me = this,
            deleteOptions, id;
        me.logger.enter("deleteSord", arguments);
        config = config || {};
        config.parentId = config.parentId || "";

        id = me.getObjId(objId);
        if (!id) {
          if (!config.silent) {
            throw "Object not found: " + objId;
          }
          me.logger.exit("deleteSord");
          return;
        }
        ixConnect.ix().deleteSord(config.parentId + "", id + "", LockC.NO, null);
        if (config.deleteFinally) {
          deleteOptions = new DeleteOptions();
          deleteOptions.deleteFinally = true;
          ixConnect.ix().deleteSord(config.parentId + "", id + "", LockC.NO, deleteOptions);
        }
        me.logger.exit("deleteSord");
      },

      /**
       * @property {Object}
       * Special folders that can be referenced by GUIDs
       */
      specialFolders: {
        administrationFolder: ["ARCPATH[(E10E1000-E100-E100-E100-E10E10E10E00)]:", "ARCPATH:/Administration"],
        bsFolder: ["ARCPATH[(E10E1000-E100-E100-E100-B10B10B10B00)]:", "{{administrationFolderPath}}/Business Solutions"],
        jcScriptingBaseFolder: ["ARCPATH[(E10E1000-E100-E100-E100-E10E10E10E11)]:", "{{administrationFolderPath}}/Java Client Scripting Base"],
        ixScriptingBaseFolder: ["ARCPATH[(E10E1000-E100-E100-E100-E10E10E10E12)]:", "{{administrationFolderPath}}/IndexServer Scripting Base"],
        webClientScriptingBaseFolder: ["ARCPATH[(E10E1000-E100-E100-E100-E10E10E10E16)]", "{{administrationFolderPath}}/Webclient Scripting Base"],
        localizationBaseFolder: ["ARCPATH[(E10E1000-E100-E100-E100-E10E10E10E1A)]:", "{{administrationFolderPath}}/Localization"],
        asBaseFolder: ["{{administrationFolderPath}}/ELOas Base"],
        wfBaseFolder: ["{{administrationFolderPath}}/ELOwf Base"],
        appsBaseFolder: ["{{administrationFolderPath}}/ELOapps"]
      },

      /**
       * Resolve special folders by GUID (see {@link #specialFolders})
       *
       *     var ressourcePath = sol.common.RepoUtils.resolveSpecialFolder("{{administrationFolderPath}}/Ressources");
       *
       * Supported variables:
       *
       *     {{administrationFolderPath}}
       *     {{bsFolderPath}}
       *     {{jcScriptingBaseFolderPath}}
       *     {{ixScriptingBaseFolderPath}}
       *     {{webClientScriptingBaseFolderPath}}
       *     {{localizationBaseFolderPath}}
       *     {{asBaseFolderPath}}
       *     {{wfBaseFolderPath}}
       *     {{appsBaseFolderPath}}
       *
       * @param {String} path Path to be resolved
       * @param {Object} paramObj Additional properties: packageName, packageBaseFolderPath
       * @return {String} Resolved Path
       */
      resolveSpecialFolder: function (path, paramObj) {
        var me = this,
            key;
        me.logger.enter("resolveSpecialFolder", arguments);
        if (!me.specialFolderPathsDeterminated) {
          me.determinateSpecialFolders();
        }
        paramObj = paramObj || {};
        for (key in me.specialFolderPaths) {
          paramObj[key] = me.specialFolderPaths[key];
        }
        if (paramObj.packageName) {
          if (paramObj.packageBaseFolderPath) {
            paramObj.packageBaseFolderPath = sol.create("sol.common.Template", { source: paramObj.packageBaseFolderPath, isRepoPath: true }).apply(paramObj);
          } else {
            paramObj.packageBaseFolderPath = paramObj.bsFolderPath;
          }
          paramObj.packageFolderPath = paramObj.packageBaseFolderPath + me.pilcrow + paramObj.packageName;
        }
        path = sol.create("sol.common.Template", { source: path, isRepoPath: true }).apply(paramObj);
        me.logger.exit("resolveSpecialFolder", path);
        return path;
      },

      /**
       * @private
       * Determinates special folders by its GUID or alternatively by a default path
       */
      determinateSpecialFolders: function () {
        var me = this,
            paths, path, folderKey, i;
        me.logger.enter("determinateSpecialFolders", arguments);
        me.specialFolderPaths = {};
        for (folderKey in me.specialFolders) {
          paths = me.specialFolders[folderKey];
          for (i = 0; i < paths.length; i++) {
            path = paths[i].replace("/", me.pilcrow);
            if (path.indexOf("{{") > -1) {
              path = sol.create("sol.common.Template", { source: path }).apply(me.specialFolderPaths);
            }
            try {
              ixConnect.ix().checkoutSord(path + "", SordC.mbOnlyId, LockC.NO);
              break;
            } catch (ignore) {
              // Object not found
            }
          }
          me.specialFolderPaths[folderKey + "Path"] = path;
        }
        me.specialFolderPathsDeterminated = true;
        me.logger.exit("determinateSpecialFolders");
      },

      /**
       * Checks the version of ELO components
       * @param {String} currentVersionString
       * @param {String} requiredVersionString
       * @return {Boolean} Return true if the current version is equal or higher then the required version
       */
      checkVersion: function (currentVersionString, requiredVersionString) {
        var me = this,
            result = true,
            currentRegex, requiredRegex, currentVersionMatch, requiredVersionMatch, currentPart, requiredPart;

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

        currentRegex = /([0-9]+(\\.[0-9]+)*)/g;
        requiredRegex = /([0-9]+(\\.[0-9]+)*)/g;
        currentVersionMatch = currentRegex.exec(currentVersionString);
        requiredVersionMatch = requiredRegex.exec(requiredVersionString);

        while (requiredVersionMatch !== null) {
          currentPart = (currentVersionMatch) ? parseInt(currentVersionMatch[0], 10) : 0;
          requiredPart = parseInt(requiredVersionMatch[0], 10);
          if (requiredPart > currentPart) {
            result = false;
            break;
          } else if (requiredPart < currentPart) {
            result = true;
            break;
          }
          currentVersionMatch = currentRegex.exec(currentVersionString);
          requiredVersionMatch = requiredRegex.exec(requiredVersionString);
        }

        me.logger.exit("checkVersion", result);

        return result;
      },

      /**
       * Checks the versions of ELO components based on the main version.
       * For each main version can be a minimum requirement.
       * If there is no specific requirement for the main current main version, the highest required main version will be checked.
       *
       * Examples:
       *     sol.common.RepoUtils.checkVersions("9.03.26", ["9.03.021", "10.01.044"]);  // => true  (min requirement for ELO 9 satisfied)
       *     sol.common.RepoUtils.checkVersions("10.01.38", ["9.03.021", "10.01.044"]); // => false (min requirement for ELO 10 not satisfied)
       *     sol.common.RepoUtils.checkVersions("11.00.02", ["9.03.021", "10.01.044"]); // => true  (no min requirement for ELO 11, but version is higher than '10.01.044')
       *
       * @param {String} currentVersionString
       * @param {String[]} requiredMainVersionStrings
       * @return {Boolean} Return true if the current version is equal or higher then the required version
       */
      checkVersions: function (currentVersionString, requiredMainVersionStrings) {
        var me = this,
            mainVersionLookup = {},
            highestMainVersion = 0,
            getMainVersion, currentMainVersion, requiredVersionForMainVersion;

        getMainVersion = function (version) {
          return +version.substring(0, version.indexOf("."));
        };

        currentMainVersion = getMainVersion(currentVersionString);

        requiredMainVersionStrings.forEach(function (requiredVersion) {
          var requiredMainVersion = getMainVersion(requiredVersion);
          mainVersionLookup[requiredMainVersion] = requiredVersion;
          highestMainVersion = (highestMainVersion < requiredMainVersion) ? requiredMainVersion : highestMainVersion;
        });

        requiredVersionForMainVersion = mainVersionLookup[currentMainVersion] || mainVersionLookup[highestMainVersion];

        return me.checkVersion(currentVersionString, requiredVersionForMainVersion);
      },

      /**
       * Returns the object IDs of the repository path elements in ascending order
       * @param {String} objId object ID
       * @return {Array} Array of object IDs
       */
      getRepoPathObjIds: function (objId) {
        var me = this,
            sord, objIds, i,
            repoPathElements, repoPathElement;
        me.logger.enter("getRepoPathObjIds", arguments);

        sord = ixConnect.ix().checkoutSord(objId, new SordZ(SordC.mbRefPaths), LockC.NO);
        objIds = [objId];
        if (!sord.refPaths || (sord.refPaths.length == 0)) {
          throw "sord.refPaths is empty";
        }

        objIds = [objId];
        repoPathElements = sord.refPaths[0].path;
        for (i = repoPathElements.length - 1; i >= 0; i--) {
          repoPathElement = repoPathElements[i];
          objIds.push(repoPathElement.id);
        }
        me.logger.exit("getRepoPathObjIds", objIds);
        return objIds;
      },

      /**
       * Finds a valid parent of a sord with specified index field
       * @param {String} objId Object ID
       * @param {String} type Name of group
       * @param {String[]} values Values
       * @return {de.elo.ix.client.Sord.id} objId
       */
      getValidParent: function (objId, type, values) {
        var me = this,
            sord;
        me.logger.enter("getValidParent", arguments);

        if (typeof values == "string") {
          values = [values];
        }

        sord = sol.common.RepoUtils.findInHierarchy(objId, { objKeyName: type, objKeyValues: values });
        if (sord != undefined) {
          me.logger.exit("getValidParent", sord.id);
          return sord.id;
        } else {
          me.logger.exit("getValidParent", null);
          return null;
        }
      },

      /**
       * Finds a sord in the hierarchy by the index field 'SOL_TYPE'
       * @param {String} objId Object ID
       * @param {String[]} values Values
       * @param {Object} config Configuration
       * @param {de.elo.ix.client.IXConnection} config.connection Index server connection
       * @return {de.elo.ix.client.Sord} Sord
       */
      findObjectTypeInHierarchy: function (objId, values, config) {
        var me = this,
            _result;
        me.logger.enter("findObjectTypeInHierarchy", arguments);
        config = config || {};
        _result = me.findInHierarchy(objId, { objKeyName: "SOL_TYPE", objKeyValues: values, connection: config.connection });
        me.logger.exit("findObjectTypeInHierarchy", _result);
        return _result;
      },

      /**
       * Find a sord in the hierarchy
       * @param {String} objId Object ID
       * @param {Object} config Optional parameters
       * @param {String[]} config.sordTypeNames Name of sord types
       * @param {String} config.objKeyName Name of the oject key
       * @param {String[]} config.objKeyValues Values of the object key
       * @param {de.elo.ix.client.IXConnection} config.connection Index server connection
       * @param {de.elo.ix.client.SordZ} config.sordZ Element selector
       * @param {Boolean} config.throwException If true a exception is thrown if no sord was found
       * @return {de.elo.ix.client.Sord} Sord
       */
      findInHierarchy: function (objId, config) {
        var me = this,
            conn, objIds, sords, i, sord, j, sordTypeIds, sordTypeId,
            objKeyName, objKeyValues, objKeyValue, currentValue;
        me.logger.enter("findInHierarchy", arguments);

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

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

        if (config.sordTypeNames) {
          if (!sol.common.ObjectUtils.isArray(config.sordTypeNames)) {
            throw "Sord type names must be an array";
          }

          config.sordZ = config.sordZ || SordC.mbLean;

          sordTypeIds = config.sordTypeNames.map(function (sordTypeName) {
            return sol.common.SordTypeUtils.getSordTypeId(sordTypeName);
          });
        }

        if (config.objKeyName) {
          if (!sol.common.ObjectUtils.isArray(config.objKeyValues)) {
            throw "Objkey values must be an array";
          }

          config.sordZ = config.sordZ || SordC.mbAllIndex;
        }

        config.sordZ = config.sordZ || SordC.mbAll;

        objIds = me.getRepoPathObjIds(objId);
        objKeyName = config.objKeyName;
        objKeyValues = config.objKeyValues;

        sords = me.getSords(objIds, { sordZ: config.sordZ, keepOrder: true, connection: conn });
        for (i = 0; i < sords.length; i++) {
          sord = sords[i];
          if (sordTypeIds) {
            for (j = 0; j < sordTypeIds.length; j++) {
              sordTypeId = sordTypeIds[j];
              if (sord.type == sordTypeId) {
                me.logger.exit("findInHierarchy", sord);
                return sord;
              }
            }
          }
          if (objKeyName) {
            currentValue = sol.common.SordUtils.getObjKeyValue(sord, objKeyName);
            if (currentValue) {
              for (j = 0; j < objKeyValues.length; j++) {
                objKeyValue = objKeyValues[j];
                if (objKeyValue == currentValue) {
                  me.logger.exit("findInHierarchy", sord);
                  return sord;
                }
              }
            }
          }
        }
        if (config.throwException) {
          throw "No appropriate predecessor found: objId=" + objId + ", config=" + sol.common.JsonUtils.stringifyAll(config);
        }
        me.logger.exit("findInHierarchy");
      },

      /**
       * Moves documents to a given storage path
       * @param {String} startObjId Start object ID
       * @param {String} dstStoragePathId
       */
      moveToStoragePath: function (startObjId, dstStoragePathId) {
        var me = this,
            navInfo, procInfo, jobState;
        me.logger.enter("moveToStoragePath", arguments);

        if (!startObjId) {
          throw "Start object ID is empty";
        }

        if (!dstStoragePathId) {
          throw "Storage path name is empty";
        }

        navInfo = new NavigationInfo();
        navInfo.startIDs = [startObjId];

        procInfo = new ProcessInfo();
        procInfo.desc = "Move to storage path";
        procInfo.errorMode = ProcessInfoC.ERRORMODE_SKIP_PROCINFO;

        procInfo.procMoveDocumentsToStoragePath = new ProcessMoveDocumentsToStoragePath();
        procInfo.procMoveDocumentsToStoragePath.pathId = dstStoragePathId;

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

        sol.common.AsyncUtils.waitForJob(jobState.jobGuid);
        me.logger.exit("moveToStoragePath");
      },

      /**
       * Sets a session option
       * @param {Number} sessionOption Session option
       * @param {Boolean|String} value Value
       */
      setSessionOption: function (sessionOption, value) {
        var me = this,
            sessionOptions = {};

        if (typeof sessionOption == "undefined") {
          throw "Option is empty";
        }

        if (typeof value == "undefined") {
          throw "Value is empty";
        }

        if (typeof value == "boolean") {
          value = value ? "true" : "false";
        }

        sessionOptions[sessionOption] = value;

        me.setSessionOptions(sessionOptions);
      },

      /**
       * Sets session options
       * @param {Object} newOptions Options, e.g. { SessionOptionsC.START_DOC_WORKFLOWS: true }
       */
      setSessionOptions: function (newOptions) {
        var sessionOptions, sessionOptionsObj, key, currOption, newValue;

        if (!newOptions) {
          throw "Options are empty";
        }

        sessionOptions = ixConnect.ix().sessionOptions;
        sessionOptionsObj = sol.common.ObjectUtils.getObjectFromArray(sessionOptions.options, "key");

        for (key in newOptions) {
          if (newOptions.hasOwnProperty(key)) {
            currOption = sessionOptionsObj[String(key)];
            newValue = newOptions[key];
            if (currOption) {
              currOption.value = newValue;
            } else {
              sessionOptionsObj[String(key)] = new KeyValue(key, newValue);
            }
          }
        }
        sessionOptions.options = sol.common.ObjectUtils.getValues(sessionOptionsObj);
        ixConnect.ix().setSessionOptions(sessionOptions);
      },

      /**
       * Get session options
       * @returns {Object}
       */
      getSessionOptions: function () {
        var sessionOptions,
            sessionOptionsObj = {},
            i, keyValue;

        sessionOptions = ixConnect.ix().sessionOptions;

        for (i = 0; i < sessionOptions.length; i++) {
          keyValue = sessionOptions[i];
          sessionOptionsObj[keyValue.key + ""] = keyValue.value + "";
        }

        return sessionOptionsObj;
      },

      /**
       * Creates a new connection factory
       * @param {java.util.Properties} connProps Connection properties
       * @param {java.util.Properties} sessOpts Session options
       * @param {Object} overrideParams Overide Parameters
       * @param {de.elo.ix.client.IXConnection} overrideParams.conn Connection
       * @param {Object} overrideParams.connProps Change connection properties
       * @param {Number} overrideParams.timeoutSeconds Timeout seconds
       * @returns {Object}
       *
       * Example:
       *
       *     conn = sol.common.RepoUtils.createConnFact(connProps, sessOpts, {
       *       timeoutSeconds: 300
       *     });
       */
      createConnFact: function (connProps, sessOpts, overrideParams) {
        var connFact, key;

        connProps = connProps || new java.util.Properties();
        sessOpts = sessOpts || new java.util.Properties();

        overrideParams = overrideParams || {};
        overrideParams.connProps = overrideParams.connProps || {};

        for (key in overrideParams.connProps) {
          connProps.setProperty(key, overrideParams.connProps[key]);
        }

        if (overrideParams.timeoutSeconds) {
          connProps.setProperty(IXConnFactory.PROP_TIMEOUT_SECONDS, overrideParams.timeoutSeconds + "");
        }

        connFact = new IXConnFactory(connProps, sessOpts);

        return connFact;
      },

      /**
       * Returns the color ID
       * @param {String} colorName Color name
       * @return {String} Color ID
       */
      getColorId: function (colorName) {
        var me = this,
            color;

        if (!me.colors) {
          me.readColors();
        }
        colorName = String(colorName).toLowerCase();
        color = me.colors[colorName];
        if (color) {
          return String(color.id);
        }
      },

      /**
       * Returns an object that contains all colors
       * @return {Object} Colors
       */
      readColors: function () {
        var me = this,
            colors, i, color, colorName;

        me.colors = {};

        colors = ixConnect.ix().checkoutColors(LockC.NO);
        for (i = 0; i < colors.length; i++) {
          color = colors[i];
          colorName = String(color.name).toLowerCase();
          me.colors[colorName] = color;
        }

        return me.colors;
      },

      /**
       * Adds colors, if they doesn't exist
       * @param {Array} newColors Colors, example: [{ name: "sol.solution.processed", rgb: "2129920" }]
       * @return {String[]}
       */
      addColors: function (newColors) {
        var me = this,
            createdColorNames = [];

        if (!newColors) {
          throw "New colors are empty";
        }

        newColors.forEach(function (newColor) {
          var colorData;
          if (!me.colorExists(newColor.name)) {
            colorData = new ColorData();
            colorData.id = -1;
            colorData.name = newColor.name;
            colorData.RGB = newColor.rgb;
            me.colors[newColor.name] = colorData;
            createdColorNames.push(newColor.name);
            me.colorsDirty = true;
          }
        });

        me.writeColors();
        return createdColorNames;
      },

      /**
       * Checks wether a color exists
       * @param {String} colorName Color name
       * @return {Boolean}
       */
      colorExists: function (colorName) {
        var me = this;
        if (!colorName) {
          throw "Color name is empty";
        }
        if (!me.colors) {
          me.readColors();
        }
        colorName = String(colorName).toLowerCase();
        return !!me.colors[colorName];
      },

      /**
       * Writes all colors
       */
      writeColors: function () {
        var me = this,
            colorArr;
        if (me.colorsDirty) {
          colorArr = sol.common.ObjectUtils.getValues(me.colors);
          ixConnect.ix().checkinColors(colorArr, LockC.NO);
        }
        me.colors = undefined;
        me.colorsDirty = false;
      },

      /**
       * Copy Sords
       * @param {Array|String} startIds Start object IDs
       * @param {String} newParentId New parent object ID
       * @param {Object} params parameters
       * @param {String} params.targetName Target name
       * @param {Boolean} [params.copyOnlyBaseElement=true] If true only the base element will be copied
       * @param {Boolean} [params.copyOnlyWorkversion=true] If true only the work version will be copied
       * @param {Boolean} [params.copyStructuresAndDocuments=true] If true structures and documents will be copied
       * @param {Boolean} [params.takeTargetPermissions=true] If true the target permissions will be set
       * @param {Boolean} [params.keepOriginalPermissions=false] If true the original permissions will be kept
       * @param {Boolean} [params.keepOriginalPermissions=false] If true the original permissions will be kept
       * @param {de.elo.ix.client.IXConnection} params.connection (optional) Index server connection
       * @return {Object}
       */
      copySords: function (startIds, newParentId, params) {
        var me = this,
            resultObj = {},
            navInfo, procInfo, jobState, entriesIterator, pair, dstFolderId, conn;

        params = params || {};
        params.copyOnlyBaseElement = (params.copyOnlyBaseElement == undefined) ? true : params.copyOnlyBaseElement;
        params.copyOnlyWorkversion = (params.copyOnlyWorkversion == undefined) ? true : params.copyOnlyWorkversion;
        params.copyStructuresAndDocuments = (params.copyStructuresAndDocuments == undefined) ? true : params.copyStructuresAndDocuments;
        params.takeTargetPermissions = (params.takeTargetPermissions == undefined) ? true : params.takeTargetPermissions;
        params.keepOriginalPermissions = (params.keepOriginalPermissions == undefined) ? false : params.keepOriginalPermissions;
        conn = params.connection || ixConnect;

        if (sol.common.ObjectUtils.isArray(startIds)) {
          if (startIds.length == 0) {
            throw "Start IDs array is empty";
          }
        } else {
          if (!startIds) {
            throw "Start IDs are empty";
          } else {
            startIds = [startIds];
          }
        }

        if (!newParentId) {
          throw "Parent ID is empty";
        }

        dstFolderId = me.getObjId(newParentId);

        if (!dstFolderId) {
          throw "Destination folder ID can't be found";
        }

        navInfo = new NavigationInfo();
        navInfo.startIDs = startIds;

        procInfo = new ProcessInfo();
        procInfo.desc = "Copy sords";
        procInfo.errorMode = ProcessInfoC.ERRORMODE_CRITICAL_ONLY;

        procInfo.procCopyElements = new ProcessCopyElements();
        procInfo.procCopyElements.copyOptions = new CopyOptions();
        if (params.targetName) {
          procInfo.procCopyElements.copyOptions.targetName = params.targetName;
        }

        procInfo.procCopyElements.createMapping = true;
        procInfo.procCopyElements.copyOptions.newParentId = dstFolderId;
        procInfo.procCopyElements.copyOptions.copyOnlyBaseElement = params.copyOnlyBaseElement;
        procInfo.procCopyElements.copyOptions.copyOnlyWorkversion = params.copyOnlyWorkversion;
        procInfo.procCopyElements.copyOptions.copyStructuresAndDocuments = params.copyStructuresAndDocuments;
        procInfo.procCopyElements.copyOptions.takeTargetPermissions = params.takeTargetPermissions;
        procInfo.procCopyElements.copyOptions.keepOriginalPermissions = params.keepOriginalPermissions;

        me.logger.debug(["Copy sords: startIds = '{0}', newParent.id = '{1}'", startIds, newParentId]);

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

        jobState = sol.common.AsyncUtils.waitForJob(jobState.jobGuid, { connection: conn });

        me.logger.debug(["Job '{0}' finished: jobState.countProcessed = '{1}', jobState.countErrors = '{2}'", procInfo.desc, jobState.countProcessed, jobState.countErrors]);

        entriesIterator = jobState.procInfo.procCopyElements.copyResult.mapIdsSource2Copy.entrySet().iterator();

        while (entriesIterator.hasNext()) {
          pair = entriesIterator.next();
          resultObj[String(pair.key)] = String(pair.value);
        }

        return resultObj;
      },

      /**
       * Detects the runtime context
       * @return {String} Context, ´JC´, ´AS´ or ´IX´
       */
      detectScriptEnvironment: function () {
        if (typeof workspace != "undefined") {
          return "JC";
        } else if (typeof emConnect != "undefined") {
          return "AS";
        } else if ((typeof $ENV != "undefined") || (typeof EloShellScriptRunner != "undefined")) {
          return "SH";
        } else {
          try {
            java.lang.Class.forName("de.elo.ix.jscript.DBConnection");
            return "IX";
          } catch (ignore) {
            // ignore
          }
        }
        throw "Can't determinate runtime context.";
      },

      /**
       * Moves Sords
       * @param {Array} objIds Object IDs
       * @param {String} dstFolderId Destination folder ID
       * @param {Object} params parameters
       * @param {String} [params.manSortIdx=-1] Manually determine the position
       * @param {String} [params.adjustAclOverwrite=true] Adjust the ACL
       */
      moveSords: function (objIds, dstFolderId, params) {
        var conn, copyInfo, i, objId;

        if (!objIds || (objIds.length == 0)) {
          return;
        }

        if (!dstFolderId) {
          throw "Destination folder ID is empty";
        }

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

        params = params || {};
        copyInfo = new CopyInfo();
        copyInfo.manSortIdx = (typeof params.manSortIdx == "undefined") ? -1 : params.manSortIdx;
        copyInfo.adjustAclOverwrite = (typeof params.adjustAclOverwrite == "undefined") ? true : params.adjustAclOverwrite;

        for (i = 0; i < objIds.length; i++) {
          objId = objIds[i];
          conn.ix().copySord(dstFolderId, objId, copyInfo, CopySordC.MOVE);
        }
      },

      /**
       * Creates an external link
       * @param {Object} params Parameters
       * @param {String} params.objId Object ID
       * @param {String} params.limitTo Limit to
       * @param {String} [params.limitToUnit=d] Limit to unit, e.g. days
       * @param {Number} params.times times Times
       * @param {Boolean} [params.escapeXml=false] Escape the URL for use in XML
       * @return {String} URL
       */
      createExternalLink: function (params) {
        var downloadOptions, publicDownload, expiration, expirationIso, url;

        params = params || {};
        params.limitToUnit = params.limitToUnit || "d";
        params.times = params.times || java.lang.Integer.MAX_VALUE;

        if (!params.objId) {
          throw "Object ID is missing";
        }

        // eslint-disable-next-line no-undef
        downloadOptions = new PublicDownloadOptions();
        downloadOptions.objId = params.objId;

        if (params.limitTo) {
          expiration = moment().add(params.limitTo, params.limitToUnit);
          expirationIso = expiration.format("YYYYMMDDHHmmss");
          downloadOptions.expiration = expirationIso;
        }

        if (params.times) {
          downloadOptions.remaining = params.times;
        }
        publicDownload = ixConnect.ix().insertPublicDownload(downloadOptions);

        url = publicDownload.url;

        if (params.escapeXml) {
          try {
            url = Packages.org.apache.commons.text.StringEscapeUtils.escapeXml11(url) + "";
          } catch (ex) {
            url = Packages.org.apache.commons.lang.StringEscapeUtils.escapeXml(url) + "";
          }
        }

        return url;
      },

      /**
       * Returns object IDs by a given ´findInfo´ object
       * @param {de.elo.ix.client.FindInfo} findInfo Find info
       * @param {Object} params Parameters
       * @param {String} params.findMax
       * @param {de.elo.ix.client.SordZ} params.sordZ SordZ
       * @return {Array}
       */
      findIds: function (findInfo, params) {
        var idx = 0,
            ids = [],
            findResult, i;

        if (!findInfo) {
          throw "FindInfo is empty";
        }

        params = params || {};
        params.findMax = params.findMax || 1000;
        params.sordZ = params.sordZ || SordC.mbOnlyId;

        findResult = ixConnect.ix().findFirstSords(findInfo, params.findMax, params.sordZ);

        ids = [];

        while (true) {
          for (i = 0; i < findResult.ids.length; i++) {
            ids.push(findResult.ids[i] + "");
          }
          if (!findResult.moreResults) {
            break;
          }
          idx += findResult.ids.length;
          findResult = ixConnect.ix().findNextSords(findResult.searchId, idx, params.findMax, params.sordZ);
        }
        ixConnect.ix().findClose(findResult.searchId);

        return ids;
      },

      /**
       * Returns IX options
       * @param {String} key Key
       * @return {Object} entry Entry
       * @return {String} Value
       */
      getIxOption: function (key) {
        var me = this,
            ixOptions, i, ixOption, ixId;

        if (!key) {
          throw "IX option key is empty";
        }

        ixOptions = me.getIxOptions();

        ixOptions = ixOptions.filter(function (entry) {
          return (entry[1] == key);
        });

        ixId = me.getIxId();

        for (i = 0; i < ixOptions.length; i++) {
          ixOption = ixOptions[i];
          if (ixOption[0] == ixId) {
            return ixOption[2];
          }
        }

        for (i = 0; i < ixOptions.length; i++) {
          ixOption = ixOptions[i];
          if (ixOption[0] == "_ALL") {
            return ixOption[2];
          }
        }
      },

      /**
       * Returns the IX options
       * @return {Array} IX options
       */
      getIxOptions: function () {
        var i, keyValuePairs, keyValuePair, completeKey, keyGroups, key, ixId,
            ixOptions = [];

        keyValuePairs = ixConnect.ix().checkoutMap(MapDomainC.DOMAIN_IX_OPTIONS, null, null, LockC.NO).items;

        for (i = 0; i < keyValuePairs.length; i++) {
          keyValuePair = keyValuePairs[i];
          completeKey = keyValuePair.key + "";
          keyGroups = completeKey.match(/(?:\[)(.+)(?:\])(.+)/);
          key = keyGroups[2];
          ixId = keyGroups[1];
          ixOptions.push([ixId, key, keyValuePair.value + ""]);
        }

        return ixOptions;
      },

      /**
       * Returns the IX ID
       * @return {String} IX ID
       */
      getIxId: function () {
        var serverInfo, ixId;

        serverInfo = ixConnect.ix().serverInfo;
        ixId = serverInfo.instanceName + "";

        return ixId;
      },

      /**
       * Calculates the next version number of a document.
       *
       *     newVersion = sol.common.RepoUtils.calcNextVersion("4711", 1);
       *
       * @param {String} objId Object ID
       * @param {Number} increaseBy (optional) Increase by
       * @return {String} New version
       */
      calcNextVersion: function (objId, increaseBy) {
        var me = this,
            sord, lastVersion, newVersion, posDot, posComma, posSpace, pos, left, right, i, char, chars, lastNumericChar;

        increaseBy = increaseBy || 1;

        me.logger.enter("calcNextVersion", arguments);
        if (!objId) {
          throw "ObjId is empty";
        }

        increaseBy = parseInt(increaseBy, 10);
        if (isNaN(increaseBy)) {
          throw "increaseBy is not a number";
        }

        sord = ixConnect.ix().checkoutSord(objId, EditInfoC.mbSord, LockC.NO).sord;
        lastVersion = sord.docVersion.version + "";

        if (lastVersion) {
          posDot = lastVersion.lastIndexOf(".");
          posComma = lastVersion.lastIndexOf(",");
          posSpace = lastVersion.lastIndexOf(" ");

          newVersion = parseFloat(lastVersion) + increaseBy;

          if (isNaN(newVersion) || posDot > -1 || posComma > -1 || posSpace > 1) {
            pos = Math.max(posDot, posComma);
            pos = Math.max(pos, posSpace);

            left = lastVersion.substring(0, pos + 1);
            right = lastVersion.substring(pos + 1);

            right = parseInt(right, 10) + increaseBy;
            if (isNaN(right)) {
              chars = [];
              for (i = lastVersion.length; i > 0; i--) {
                char = lastVersion[i - 1];
                if (!isNaN(parseInt(char, 10))) {
                  chars.push(char);
                } else {
                  lastNumericChar = i;
                  break;
                }
              }

              if (chars.length > 0) {
                right = chars.reverse().join("");
                right = parseInt(right, 10) + increaseBy;

                left = lastVersion.substring(0, lastNumericChar);
                newVersion = left + right;
              }
            } else {
              newVersion = left + right;
            }
          }
        }

        if (!newVersion) {
          newVersion = lastVersion ? (lastVersion + " 1") : "1";
        }

        me.logger.exit("calcNextVersion", newVersion);
        return newVersion + "";
      }
    });