//@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 + "";
  }
});