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

//@include lib_Class.js
//@include lib_sol.common.ObjectUtils.js
//@include lib_sol.common.RepoUtils.js
//@include lib_sol.common.StringUtils.js
//@include lib_sol.common.AclUtils.js
//@include lib_sol.common.ObjectFormatter.js

/**
 * Retrieves sords using a search (findByIndex) or by predefined ids and formats the retrieved sords according to
 * the output parameter. If fuzzy search (getContextTerms) is used, the output parameter is ignored since this
 * search variant returns value groups just like an SQL GROUP BY query.
 *
 * This service is optimized by analyzing defined search terms and output options to retrieve the results in the
 * fastest way possible. Additionally, this service caches a subset of analyzed parameters on demand to further
 * improve execution time.
 *
 * ### Searching
 *
 * Two search variants are provided. findByIndex and getContextTerms
 *
 * #### Search Definition
 *
 * Both search variants use the same search definition. This makes it easier to switch between variants if needed.
 * FindByIndex is used as a default.
 *
 * A search definition contains a `masks` property and a `search` property, which in turn contains additional search criteria.
 *
 * The masks property is an Array of Strings, where each string must define a mask to include in the search.
 *
 *     masks: ["My Mask", "My Mask 2"]
 *
 * The search `property` can contain search criteria.
 *
 *     search: [
 *       { key: "SOL_TYPE", value: "RECRUITING_CANDIDATE" },
 *       { key: "DEPARTMENTS", value: ["Sales", "Purchasing"] },
 *       { key: "ACTIVITYSTATUS", value: "A - *"},
 *       { key: "SOL_REFERENCE", value: "REF-0001¶REF-0002"}
 *     ]
 *
 * Hint: Sordprovider search values provides MultiIndex strings. If the string contains a pilcrow
 * the value will be converted to ["REF-0001", "REF-0002"]
 *
 * You must also define an output definition. (see further below)
 *
 * To search for a date field, use the standard ELO syntax (YYYYMMDDHHMM or YYYYMMDD), but also define the property "type: date".
 *
 *     "search": [
 *       { "key": "INVOICE_DATE", "value": "197310160000...202309300000", "type": "date" }, // range
 *       { "key": "RETRIEVAL_DATE", "value": "-0000-00-30...+0000-00-00", "type": "date" }  // range using offset (-30days to today)
 *     ]
 *
 * If you need to search for an xDateIso or iDateIso, simply define XDATEISO or IDATEISO as the search-key.
 *
 * Date searches are only implemented for FindByIndex searches. Fuzzy/FindDirect searches will not work!
 *
 * If you need to search for a name, simply define name as the search-key
 *
 *    "search": [
 *        { "key": "name", value: "shortDescription"}
 *    ]
 *
 * Attention: A search via the short name always leads to a full table scan on the database.
 * The query is very slow with large databases.
 *
 *
 * #### Fuzzy Search (getContextTerms)
 *
 * Fuzzy search can be used if the results need to be grouped.
 * Fuzzy means, the result is not guaranteed to reflect the current state of the ELO sql database.
 * Fuzzy search and therefore getContextTerms queries iSearch which uses another higher performance
 * database containing possibly out of date values. (Re-indexing takes place about every minute)
 *
 * If you need 100% valid results you must not use fuzzy search. If speed matters most and you only need
 * an estimated grouped result, you may use fuzzy search.
 *
 * Example for a fuzzy search grouped by field/property "a"
 *
 *     results before grouping:
 *       [{ a: "test", b: "rest" }, { a: "now", b: "test" }, { "a: test", c: "what" }]
 *     results after grouping:
 *       {test: 2, now: 1}
 *
 * Note: when using fuzzy search, you only have access to the `results after grouping`.
 *
 * To activate fuzzy search, define
 *
 *     options: {
 *       fuzzy: { groupBy: { type: "GRP", key: MY_GROUPING_FIELD  } } // only GRP supported for now
 *     }
 *
 * You can limit the number of returned groups by defining
 *
 *     options: {
 *       fuzzy: { groupBy: { type: "GRP", key: MY_GROUPING_FIELD  }, maxGroups: 20 }
 *     }
 *
 * ##### Case Sensitivity
 *
 * To turn off case sensitivity, define `ignoreCase: true` in the search definition for the respective field:
 *
 *     search: [
 *       { key: "SUBJECT", value: "Progress", "ignoreCase": true },
 *       { key: "DEPARTMENTS", value: ["Sales", "Purchasing"] }
 *     ]
 *
 * would return e.g.
 *
 *     {Progress: 2, PROGRESS: 5, progreSs: 1}
 *
 * #### FindDirect search
 *
 * To use findDirect as the search mode (will additionally search in memos, fulltext,...), pass
 *
 *     options: {
 *       findDirect: true
 *     }
 *
 * If you need to define a custom query term, pass
 *
 *     options: {
 *       query: "your term"
 *     }
 *
 * If a query is passed, you don't have to pass `findDirect: true` also.
 *
 * #### Tokenization (findDirect only)
 *
 * To search in the tokenized elastic search field, define `tokenized: true` in the search definition for the respective field:
 *
 *     search: [
 *       { key: "SUBJECT", value: "Progress", "tokenized": true },
 *       { key: "DEPARTMENTS", value: ["Sales", "Purchasing"] }
 *     ]
 *
 * #### Sanitize value
 * By default search Values will be sanitized to remove possible escape characters (hyphen), blank values and double/triple whitespaces.
 * To disable sanitation of these values you can disable it seperatly. Please be careful, without sanitation it will be easier to generate wrong search queries.
 *
 * E.g.
 *      search: [
 *        { key: "A_KEY", value: "A_VALUE-WITH_HYPHEN", sanitize: { escapeValues: false } },
 *        { key: "A_KEY", value: "A_VALUE-WITH_HYPHEN", sanitize: { blankValues: false } },
 *        { key: "A_KEY", value: "A_VALUE-WITH_HYPHEN", sanitize: { removeDupWhiteSpaces: false } }
 *      ]
 *
 *
 * #### Paged Searching
 *
 * If you know there will be lots of results, it may make sense to use a paged search.
 *
 * To activate paging, define
 *
 *     options: { paging: true }
 *
 * The service result will then contain an additional property `searchId`, which is a unique search id.
 *
 *     {
 *       searchId: "(141DD-12QW-46GS-9928JJ)",
 *       moreResults: true,
 *       sords: [...]
 *     }
 *
 * If you want to fetch the next batch of results, call the service again by replacing the `paging: true` with `searchId` and the received id:
 *
 *     {
 *       options: { searchId: "(141DD-12QW-46GS-9928JJ)" }
 *     }
 *
 * The last batch of results will neither contain a search id nor the moreResults property.
 *
 * ##### Controlling paging
 *
 * Define the following, to retrieve 10 search results starting from the 131st result.
 *
 *     {
 *       options: { searchId: "(141DD-12QW-46GS-9928JJ)", startPagingFrom: 130, pageSize: 10 }
 *     }
 *
 * You can define any integer greater than -1 as startPagingFrom.
 * When there are no more results, instead of throwing an exception, an empty result.sords array will be returned.
 *
 * ### Page sizes
 *
 * You can define how many results should be delivered per batch by defining a `pageSize`
 *
 *     {
 *       options: { pageSize: 100 }
 *     }
 *
 * Page sizes greater than 1000 are not allowed and will automatically be reduced to 1000.
 *
 * To end a paged search/close a search, pass
 *
 *     {
 *       options: { searchId: "(141DD-12QW-46GS-9928JJ)", endPaging: true }
 *     }
 *
 * This will return `{ sords: [] }`.
 *
 * If you try to fetch results for a searchId which has been closed, an exception will be thrown.
 *
 * ### Formatting
 *
 * Search results will be processed by a formatter. The formatter options must be provided in the `output` parameter.
 *
 *     output: [
 *       { source: { type: "SORD", "key": "guid" }, target: { prop: "id" } },
 *       { source: { type: "MAP", "key": "MY_MAP_FIELD" }, target: { prop: "myprop" } }
 *     ]
 *
 * If 3 sords were found during search, these formatter options would result in
 *
 *     {
 *       sords: [
 *         { id: "(345AD-DFS22-2FSF3-121WD)", myprop: "Testvalue" },
 *         { id: "(3573D-DF6522-2FAVF3-1SDD)", myprop: "Another value" }
 *         { id: "(3J5AD-DFQQ2-2FSFY-12G)", myprop: "Result 3" }
 *       ]
 *     }
 *
 * #### Wildcards for retrieving an unknown amount of MAP/BLOB fields having prefix x
 *
 * You can include MAP-fields in the output using a wildcard. This can be handy if you don't
 * know how many MAP-fields with a specific prefix will exist in the sord.
 *
 *     output: [
 *       { source: { type: "MAP", "key": "LOCALE_*" }, target: { prop: "*" } }
 *     ]
 *
 * Hint: `prop` will be ignored, you can define whatever you like. However, to make clear what you want to do, please define `"*"` or similar.
 *
 * This would output e.g.:
 *
 *     {
 *       sords: [
 *         { LOCALE_APPLE: "sol.lang.apple", LOCALE_NEW: "sol.lang.new" },
 *         { LOCALE_APPLE: "sol.lang.apple", LOCALE_NEW: "sol.lang.new", LOCALE_TREE: "sol.lang.tree" },
 *       ]
 *     }
 *
 * #### Receive permissions on each result sord object
 *
 * If you want to checkout aclItems, `SordProvider` supports the concepts of converter. At the moment `SordProvider`
 * supports two converters to convert `sord.aclItems`.
 *
 * Either you can return the accesscode of the sord object or you can define `aclItems` output definition
 * to return permission object like in the example.
 *
 * When you define aclItems without an explicit converter, each time the converter for the access code is used
 * automatically. It will be thrown an exception if the converter is unknown.
 *
 * Hint: converter prop of the output definition will only be used for sordKeys currently.
 * Another types are currently not supported like objKeys, mapKeys, formBlobKeys and so on.
 *
 *  output: [
 *       { source: { type: "SORD", "key": "guid" }, target: { prop: "id" } },
 *       { source: { type: "SORD", "key": "aclItems", converter: "ACCESS_RIGHTS_CONVERTER" }, target: { prop: "permission" } }
 *  ]
 *
 * The output will be:
 *
 *      {
 *       sords: [
 *         { id: "(345AD-DFS22-2FSF3-121WD)", permission: { r: true, w: true, d: true, e: true, l: true, p: true} }
 *       ]
 *     }
 *
 * #### Format as TemplateSord
 *
 * Define the parameter
 *
 *     {
 *       options: { formatAsTemplateSord: true }
 *     }
 *
 * When this parameter is defined, the `target.prop` name is ignored by default. A template sord based on the original fieldnames will be created.
 *
 * To use the name defined in `target.prop`, define
 *
 *     {
 *       options: { formatAsTemplateSord: true, ignorePropertyNames: false }
 *     }
 *
 * #### Adding properties with custom values
 *
 * You may want to add a fixed property to every result. This is possible by only defining a target.
 *
 *     output: [
 *       { target: { prop: "myspecialprop", value: "My Fixed Value" } }
 *     ]
 *
 * #### Format with ObjectFormatter
 *
 * You can use ObjectFormatters as defined in lib_sol.common.ObjectFormatter to format the output.
 * To do so, define formatter and config within the `output` parameter, e.g.:
 *
 *     output: {
 *        formatter: 'sol.common.ObjectFormatter.TemplateSord',
 *        config: {}
 *     }
 *
 * the defined option parameter regarding output formatting "formatAsTemplateSord" and "ignorePropertyNames" will be ignored.
 *
 * ### Skipping search and providing ids directly
 *
 * If you only want to use the formatting facilities of this service, you can omit the search definition and
 * just provide objIds/guids directly. The service will automatically determine, whether you passed guids or objIds.
 *
 *     ids: ["12345", "43241"]
 *
 * ObjIds and guids can't be mixed.
 *
 * ### Filtering results
 *
 * When you perform a normal search, results will already be filtered by the search criteria. However, you may also want to filter e.g. based on
 * a search result's MAP field. This is made possible by the `filter` parameter.
 *
 * Also, if you pass guids or objIds instead of searching, you would always receive all of the formatted sords, no matter, which criteria they have.
 * The same filter mechanism can be applied to this use-case.
 *
 * Only properties which are defined as a `target.prop` in the `output`-definition can be filtered.
 *
 * If a sord matches the filter, it will be kept. If it does not match the filter, it will be removed from the result-set.
 *
 * Just like the output-parameter, if using an optimized search, the parsed filter will be cached. This also means, that you can omit it
 * on consecutive calls since it will be ignored anyways.
 * (This can be done e.g. by adding it to the array parameter of the ixUtils.optimizedExecute function.)
 *
 * #### Applying a filter
 *
 *     filter: [
 *       { prop: "soltype", value: "RECRUIT*_*" },
 *       { prop: "status", value: ["A -*", "B -*", "C -*"] }
 *     ]
 *
 * This example would only return sords having a soltype property value of "RECRUIT" + Any characters + "_" + Any characters
 * while also having one of the status values beginning with "A -" or "B -" or "C -".
 *
 * #### Applying a filter in TemplateSord mode
 *
 * If `formatAsTemplateSord: true` is passed as an option, you need to define the whole property path to apply a filter.
 *
 *     filter: [
 *       { prop: "objKeys.soltype", value: "RECRUIT*_*" },
 *       { prop: "mapKeys.status", value: ["A -*", "B -*", "C -*"] }
 *     ]
 *
 *
 * #### Excluding a field only used for filtering from the sord-result
 *
 * If you include a field in the output-definition, just to enable its usage in the filter, you might not want to include
 * the respective field in the end-result. Just add the `exclude:true` property to the desired definition.
 *
 *     filter: [
 *       { prop: "soltype", value: "RECRUIT*_*", "exclude": true },
 *       { prop: "status", value: ["A -*", "B -*", "C -*"] }
 *     ]
 *
 * Hint: excluding a field will not work in TemplateSord mode.
 *
 * ### Options
 *
 * #### Returning sparse results
 *
 * Returns only properties which have a value. Empty strings are not a value.
 *
 * This can further reduce network bandwith if defined.
 *
 *     options: { sparse: true }
 *
 * Hint: Caused by implementation details of the indexserver api, map and formblob values are not included
 * in the result sord in the first place, even if `sparse` is undefined or false.
 *
 * #### Allowing searches without a mask
 *
 * For security reasons, the `search` must always contain a mask. (Read: Must not contain an empty string as mask)
 *
 *     // Not allowed:
 *     1. masks: []
 *     2. masks: [""]
 *     3. masks: ["test", ""]
 *
 * You may accidentally define the third variant, which would produce duplicate results.
 *
 * To allow the second variant or just omit the masks parameter completely, alltogether
 *
 *     options: { allowEmptyMask: true }
 *
 * Tip: If a single mask is defined, the search will be fastest. If you define multiple masks or no masks, the formatter will have
 * to lookup the mask for every sord.
 *
 * #### Limit Search Results
 *
 * You know there are 100000 results in the archive, but you only want 300.
 *
 *     options: { maxResults: 300 }
 *
 * `maxResults` defaults to 5000.
 *
 * ### Optimization
 *
 * To further improve the service's performance, you can make it reuse the parsed formatter definitions.
 *
 * Attention: This should only be used, when it is guaranteed, that the output-parameters stay the same for
 * every call of the service when using the same optimization id.
 *
 * Note: Optimization can not be used with fuzzy mode. (Since there is no output parameter to cache)
 *
 * To activate optimization, define
 *
 *     {
 *       optimize: true
 *     }
 *
 * The service result will then contain an additional property `optimization`, which is a unique optimization cache Id.
 *
 *     {
 *       optimization: 59921231,
 *       sords: [...]
 *     }
 *
 * When you call the service the next time, you can omit the output parameter and replace the `true` of `optimize` by the id.
 *
 *     {
 *       optimize: 59921231
 *     }
 *
 * This will speed up the function call by some milliseconds. When searching 1000 results, this may not be a big factor,
 * but when only fetching some ids, it will not make much of a difference if you handcode the whole process or just
 * use this function.
 *
 * #### Implementing optimization in a calling class
 *
 * This is a recommendation on how to keep track of the optimization ids in your own class.
 *
 * Set up a class member
 *
 *     _optimizations: {}  // this will be your local cache for storing the optimization ids
 *
 * You can now call sol.common.IxUtils.optimizedExecute whenever you need it.
 *
 *     process: function () {
 *       var me = this, candidates, apples;
 *       candidates = sol.common.IxUtils.optimizedExecute("RF_sol_common_service_SordProvider", me.myConfigForSearchingCandidates, me._optimizations, "candids", ["output"]);
 *       apples = sol.common.IxUtils.optimizedExecute("RF_sol_common_service_SordProvider", me.myConfigForSearchingApples, me._optimizations, "appls", ["output"]);
 *       return [candidates, apples];
 *     }
 *
 * The optimizedExecute function can be used with any function implementing the same behaviour as SordProvider (taking
 * an `optimize` parameter {Boolean|String}, returning an `optimization` {String})
 *
 * The first parameter takes the RF to execute, the second parameter are the arguments passed to the RF (usually an Object).
 *
 * The third parameter defines that we would like to use _optimizations as our optimization id cache.
 *
 * The fourth parameter enables calling the function in an optimized way for different configs.
 *
 * The fifth parameter defines which config-properties should not be transfered to the RF when an optimization is available. E.g. SordProvider does not need the output
 * parameter because it caches the instructions generated from this parameter.
 *
 * You can make up any name for an optimization. However, it should be unique in your class
 * since the name decides on which optimization id will be passed to the RF.
 *
 * ### Examples
 * #### Give an objId, get a guid
 *
 *     Call
 *     { ids: ["12345"], output: [{ source: { type: "SORD", key: "guid" } }] }
 *     Result
 *     { sords: ["(345AD-DFS22-2FSF3-121WD)"] }
 *
 * #### Search without mask, return a single result
 *
 *     Call
 *     {
 *       masks: [""],
 *       search: [
 *         { key: "SOL_TYPE", value: ["RECRUITING_REQUISITION", "RECRUITING_POOL"] }
 *         { key: "RECRUITING_REQUISITION_NO", value: "0001" }
 *       ],
 *       output: [
 *         { source: { type: "SORD" , key: "guid" }, target: { prop: "guid" } },
 *         { source: { type: "GRP" , key: "RECRUITING_REQUISITION_NAME" }, target: { prop: "name" } }
 *       ],
 *       options: {
 *         allowEmptyMask: true,
 *         maxResults: 1
 *       }
 *     }
 *     Result
 *     { sords: [{ guid: "(2A15C78E-866E-D9FE-0A44-832AFFAF85EE)", name: "IT Engineer" }] }
 *
 * #### Search with multiple masks and paging, return 80 results in batches of 50
 *
 * There is one "Recruiting Requisition" in the archive and more than 79 "Recruiting Candidate"s
 *
 *     Call 1
 *     {
 *       masks: ["Recruiting Requisition", "Recruiting Candidate"],
 *       search: [{ key: "RECRUITING_REQUISITION_NO", value: "0001" }],
 *       output: [
 *         { source: { type: "SORD" , key: "guid" }, target: { prop: "guid" } },
 *         { source: { type: "SORD" , key: "name" }, target: { prop: "name" } }
 *       ],
 *       options: {
 *         paging: true,
 *         pageSize: 50
 *       }
 *     }
 *     Result 1
 *     {
 *       sords: [
 *         { guid: "(BEDCE611-7B78-9C48-E9D9-8DFE7A2ABAB3)", name: "Senior IT Engineer (0001)" },
 *         { guid: "(5C9A204C-D16C-4EC8-C5AF-5933ECA37120)", name: "Fesh, Dieter (00234)" },
 *         // 48 more ...
 *       ],
 *       searchId: "(5C041998-C430-11D8-04A8-CA1EC9CC2502)"
 *     }
 *
 *     Call 2
 *     {
 *       options: {
 *         searchId: "(5C041998-C430-11D8-04A8-CA1EC9CC2502)",
 *         startPagingFrom: 49
 *         pageSize: 50
 *       }
 *     }
 *     Result 2
 *     {
 *       sords: [
 *         { guid: "(B57CBBE6-81D8-75CC-836D-26717DDCF7FC)", name: "Monkfish, Thelonious (0005)" },
 *         { guid: "(1A6E094C-A49E-17A9-19B4-FB56A8FD4E04)", name: "Nex, Carl (0001)" }
 *         // 28 more ...
 *       ]
 *     }
 *
 * @elojc
 * @eloas
 * @eloix
 *
 * @author ESt, ELO Digital Office GmbH
 *
 * @requires sol.common.ObjectUtils
 * @requires sol.common.StringUtils
 * @requires sol.common.ObjectFormatter
 * @requires sol.common.AclUtils
 *
 */
sol.define("sol.common.SordProvider", {
  /**
   * @cfg {String} id (optional)
   * A single objId or guid. The sord is fetched, formatted and returned.
   */

  /**
   * @cfg {String[]} ids (optional)
   * An Array of objIds or guids. All sords are fetched, formatted and returned.
   */

  /**
   * @cfg {String[]} masks (optional)
   * Masks which will be included in the search (`OR` concatenation). Define an empty string as array member to include all masks.
   * If all masks are included, the `option.allowEmptyMask` must be true.
   */

  /**
   * @cfg {Object[]} search (optional)
   * @cfg {Object} search.Object
   * @cfg {String} search.Object.key name of field
   * @cfg {String|String[]} search.Object.value search value. If an array is defined, entries will be concatenated with `OR`
   */

  /**
   * @cfg {Object[]} output (optional)
   * @cfg {Object} output.Object
   * @cfg {Object} output.Object.source (optional)
   * @cfg {String} output.Object.source.key name of source field
   * @cfg {String|"SORD"|"GRP"|"MAP"|"FORMBLOB"} output.Object.source.type type of field
   * @cfg {Object} output.Object.target
   * @cfg {String} output.Object.target.prop name of target property
   * @cfg {String} output.Object.target.value (optional) value for property (useful if no `source` defined)
   */

  /**
   * @cfg {Object[]} filter (optional)
   * @cfg {Object} filter.Object
   * @cfg {String} filter.Object.prop name of the property to read. (property must be defined as a `target.prop` in the `output` parameter)
   * @cfg {String|String[]} filter.Object.value filter value. If an array is defined, entries will be concatenated with `bitwise OR`. The filter is to be understood as working like the array.filter method.
   */

  /**
   * @cfg {Object} options (optional)
   * @cfg {Boolean} [options.allowEmptyMask = false] allow `masks` array to have an empty string as member
   * @cfg {Number} [options.maxResults = 5000] maximum results for the search. if paging is active, a maximum of 1000 results is returned per paged search. Paged searches ignore this parameter.
   * @cfg {Boolean|String} [options.paging = false] (optional) if true, searches use paging and return a paging id. If the `paging` value of a result is passed, the paged search is continued.
   * @cfg {Number} [options.pageSize = 1000] (optional) if paging is active. this defines how many results will be returned per search
   * @cfg {Object} options.fuzzy (optional) use getContextTerms instead of findByIndex for searching
   * @cfg {Object} options.fuzzy.groupBy
   * @cfg {String|"GRP"} options.fuzzy.groupBy.type only "GRP" supported so far.
   * @cfg {String} options.fuzzy.groupBy.key name of field to use for grouping
   * @cfg {Number} [options.fuzzy.maxGroups = 25] (optional) maximum groups to be returned
   */

  /**
   * @cfg {Boolean|Number} [optimize = false] (optional) if true, caches analyzed parameters. If the `optimization` value of a result is passed, it will use the cached parameters instead of analyzing again.
   */

  pilcrow: String.fromCharCode(182),

  /**
   * @property {Number} [defaultMaxResults = 5000] Limits how many results will be returned by a search. Overwritten by `options.maxResults` if defined by user
   */
  defaultMaxResults: 5000,

  /**
   * @property {Number} [defaultMaxGroups = 25] Limits how many groups will be returned by a fuzzy search. Overwritten by `options.fuzzy.maxGroups` if defined by user
   */
  defaultMaxGroups: 25,

  /**
   * @property {String} [wildCard = "*"] Character which will be used as the wildcard character in fuzzy searches.
   */
  wildCard: "*",

  /**
   * @property {Number} [pageSizeMax = 1000] Hard limit. How many results are returned per findFirstSords/findNextSords. `pageSize` gets sanitized to this value if it is defined and greater
   */
  pageSizeMax: 1000,

  /**
   * @private
   * mask line mappings for faster access to GRP-field values
   */
  maskObjKeyCache: {},

  /**
   * @private
   * optimized `output` definitions
   */
  manualOptimizationCache: [],

  /**
   * @private
   * current idx and remaining desired results for each search
   */
  searchCache: {},

  /**
   * @private
   * is used as property name, if only ids are returned but direct values are set and no target.prop was defined.
   */
  fallbackIdProp: "id",

  /**
   * @private
   * Use specific SordC for these values.
   */
  BASICSORDKEYS: ["ID", "GUID", "NAME", "DESC", "MASK", "XDATEISO", "IDATEISO", "ACLITEMS"],

  /**
   * @private
   * mb map
   */
  MBS: {
    ID: SordC.mbOnlyId.getBset(),
    GUID: SordC.mbOnlyGuid.getBset(),
    NAME: SordC.mbName,
    DESC: SordC.mbDesc,
    MASK: SordC.mbMask,
    XDATEISO: SordC.mbXDateIso,
    IDATEISO: SordC.mbIDateIso,
    MIN: SordC.mbMin.getBset(),
    GRP: SordC.mbObjKeys,
    ACLITEMS: SordC.mbAclItems
  },

  /**
   * @private
   */
  CONVERTERS: {
    /**
     * Retrieve the acces code of the sord object by the aclItems
     */
    ACCESS_CODE_CONVERTER: function (sord) {
      return String(sol.common.AclUtils.getAccessCode(sord));
    },

    /**
     * Retrieve the rights object {r: true, w: true, ...} of the sord object
     */
    ACCESS_RIGHTS_CONVERTER: function (sord) {
      return sol.common.AclUtils.getAccessRights(sord);
    }
  },

  DEFAULT_CONVERTERS: {
    aclItems: "ACCESS_CODE_CONVERTER"
  },

  /**
   * @private
   * used to decide on which property should be read during formatting
   */
  SORDACCESSORS: {
    SORD: "sordKeys",
    GRP: "objKeys",
    MAP: "mapKeys",
    FORMBLOB: "formBlobs"
  },

  /**
   * @private
   * these values will be escaped in findDirect search queries
   */
  ESCAPE_VALUES: [
    "(-)"
  ],

  /**
   * @private
   * these values will be removed from findDirect search queries
   */
  BLANK_VALUES: [
    "\\[", "\\]", "\\{", "\\}", "=", "\\|", "\\(", "\\)", "\\^", "\\\"", "~", ":", "\\\\", "/", "<", ">", "\\+", "&", "!", "\\?"
  ],

  /**
   * @private
   * helpers for collectDataFromId
   */

  /**
   * @returns {de.elo.ix.Client.Sord[]} sords by objIds
   */
  getSords: function (objIds, sordC) {
    var me = this;

    return (objIds || [])
      .map(function (objId) {
        return me.getSord(objId, sordC || SordC.mbAllIndex);
      });
  },

  /**
   * @returns {de.elo.ix.Client.Sord} sord by objId
   */
  getSord: function (objId, srdC) {
    try {
      return ixConnect.ix().checkoutSord(objId, srdC, LockC.NO);
    } catch (e) {
      throw "SordProvider: could not checkout sord with objId/guid `" + objId + "`";
    }
  },

  sordKeysExtractor: function (result, sord, fields) {
    var me = this, field, i = fields.length, targetProp, ignorePropNames = me.ipn, converterFct;

    while (i--) {
      field = fields[i];
      targetProp = ignorePropNames ? field.key : (field.prop || field.key);

      converterFct = me.getOutputConverterFunction(field);

      if (converterFct) {
        // use selected converter function to convert sord prop to
        // specific result (e.g. use ACCESSRIGHTSCONVERTER) to format
        // aclItems to {r: true, w: true, ...} formatted object
        result[targetProp] = converterFct(sord);
      } else {
        result[targetProp] = String(sord[field.key] || "");
      }
    }
  },

  getOutputConverterFunction: function (field) {
    var me = this, converterFctKey, converterFct;

    if (!sol.common.StringUtils.isBlank(field.converter)) {
      converterFct = me.CONVERTERS[field.converter.toUpperCase()];

      if (!converterFct) {
        throw Error("SordProvider: converter " + field.converter + " does not exist. Only " + Object.keys(me.CONVERTERS) + " are allowed");
      }
    } else {
      converterFctKey = me.DEFAULT_CONVERTERS[field.key];
      converterFct = me.CONVERTERS[converterFctKey];
    }

    return converterFct;
  },

  objKeysExtractor: function (result, sord, fieldMapping, _accessors, mask) {
    var me = this, i = fieldMapping.length, field, fieldNo, d, dLen, str, values, pilcrow = me.pilcrow,
        key, targetProp, ignorePropNames = me.ipn,
        maskObjKeys = (me.maskObjKeyCache[mask || +(sord.mask)])
        || me.generateObjKeyLineMapping(mask || +(sord.mask));

    while (i--) {
      key = (field = fieldMapping[i]).key;
      targetProp = ignorePropNames ? key : (field.prop || key);
      if ((fieldNo = maskObjKeys[key]) !== undefined) {
        if ((dLen = (values = sord.objKeys[fieldNo].data).length) > 1) {
          for ((str = "", d = 0); d < dLen; d++) {
            str += (values[d] || "");
            (d + 1 < dLen) && (str += pilcrow);
          }
          result[targetProp] = str;
        } else {
          result[targetProp] = String(values[0] || "");
        }
      } else {
        result[targetProp] = "";
      }
    }
  },

  mapExtractor: function (sord, mapDomain, fields) {
    return Array.prototype.slice.call((ixConnect.ix().checkoutMap(mapDomain, sord.id, fields, LockC.NO)).items);
  },

  mapKeysExtractor: function (result, sord, fieldMapping, accessors) {
    var me = this, items = me.mapExtractor(sord, MapDomainC.DOMAIN_SORD, accessors.mapKeys), i = items.length, item, targetProp, ignorePropNames = me.ipn;
    while (i--) {
      item = items[i];
      targetProp = ignorePropNames ? item.key : (fieldMapping[item.key] || item.key);
      result[targetProp] = String(item.value);
    }
  },

  formBlobsExtractor: function (result, sord, fieldMapping, accessors) {
    var me = this,
        items = me.mapExtractor(sord, "formdata", accessors.formBlobs),
        i = items.length,
        item, targetProp, ignorePropNames = me.ipn, stream;
    while (i--) {
      stream = (item = items[i]).getBlobValue().getStream();
      targetProp = ignorePropNames ? item.key : (fieldMapping[item.key] || item.key);
      result[targetProp] = String(Packages.org.apache.commons.io.IOUtils.toString(stream, java.nio.charset.StandardCharsets.UTF_8));
      stream.close();
    }
  },

  formatSord: function (sord, formatterConfig, configKeys, accessors, mask) {
    var me = this, result = {}, i = configKeys.length, key;

    while (i--) {
      me[(key = configKeys[i]) + "Extractor"](result, sord, formatterConfig[key], accessors, mask);
    }

    return result;
  },

  formatSordAsTemplateSord: function (sord, formatterConfig, configKeys, accessors, mask) {
    var me = this, result = {}, i = configKeys.length, key;

    while (i--) {
      me[(key = configKeys[i]) + "Extractor"](
        (key === "sordKeys" ? result : (result[key] = {})),
        sord,
        formatterConfig[key],
        accessors,
        mask
      );
    }

    return result;
  },

  collectDataFromId: function (id, instructions, mask) {
    var me = this, sord = {};

    if (instructions.sordZ) {
      sord = me.getSord(id, instructions.sordZ);
    } else if (instructions.formatterRequired) {
      sord.id = id;
    }

    if (instructions.formatterRequired) {
      return me.getFormatter()(
        sord,
        instructions.converterConfig,
        instructions.configKeys,
        instructions.mapAccessors,
        mask
      );
    }

    return sord;
  },

  getFormatter: function () {
    var me = this;

    return me.formatAsTemplateSord
      ? me.formatSordAsTemplateSord.bind(me)
      : me.formatSord.bind(me);
  },

  setUnchangedIdsAsResult: function (resultArr, ids) {
    var me = this, i, len = ids.length;
    me.logger.debug("Mode: Input is output");

    for (i = 0; i < len; i++) {
      resultArr.push(String(ids[i]));
    }

    me.logger.debug(ids.length + " sords processed");
  },

  reinitializeFormatterSettingsIfRequired: function (instructions) {
    if (!instructions.sordZ && !instructions.converterconfig && !instructions.formatterRequired) {
      instructions.formatterRequired = true;
      instructions.sordZ = instructions.idName === "guid" ? SordC.mbOnlyGuid : SordC.mbOnlyId;
      instructions.converterConfig = { sordKeys: [{ key: instructions.idName, prop: "__id" }] };
      instructions.configKeys = ["sordKeys"];
    }
  },

  getIdsOfIds: function (resultArr, ids, instructions) {
    var me = this, i, len = ids.length;
    me.logger.debug("Mode: Get objIds of guids or guids of objIds");

    me.reinitializeFormatterSettingsIfRequired(instructions);

    for (i = 0; i < len; i++) {
      resultArr.push(me.collectDataFromId(ids[i], instructions)["__id"]);
    }

    me.logger.debug(ids.length + " sords formatted");
  },

  defaultDataCollection: function (resultArr, ids, instructions, mask) {
    var me = this;
    me.logger.debug("Mode: Default formatter");

    if (instructions.objectFormatter) {
      me.getFormattedDataCollectionFromObjectFormatter(ids, instructions)
        .map(me.pushToResults.bind(me, resultArr));
    } else {
      (ids || [])
        .map(function (id) {
          return me.collectDataFromId(id, instructions, mask);
        })
        .map(me.pushToResults.bind(me, resultArr));
    }

    me.logger.debug(ids.length + " sords formatted");
  },

  getFormattedDataCollectionFromObjectFormatter: function (ids, instructions) {
    var me = this;

    return (sol.common.ObjectFormatter.format(
      me.getObjectFormatterInput(ids, instructions)
    ) || {}).sords || [];
  },

  pushToResults: function (resultArr, formattedSord) {
    resultArr.push(formattedSord);
  },

  getObjectFormatterInput: function (ids, instructions) {
    var me = this;

    return {
      sords: {
        formatter: instructions.objectFormatter.formatter,
        data: me.getSords(ids, instructions.sordZ),
        config: instructions.objectFormatter.config
      }
    };
  },

  generateObjKeyLineMapping: function (maskName) {
    var me = this, maskLines, result = {}, i, line, DocMaskZ = new Packages.de.elo.ix.client.DocMaskZ();
    me.logger.debug("Preparing cache for mask", maskName);

    DocMaskZ.add(DocMaskC.mbLines);
    i = (maskLines = Array.prototype.slice.call(ixConnect.ix().checkoutDocMask(maskName, DocMaskZ, LockC.NO).lines)).length;

    while (i--) {
      result[(line = maskLines[i]).key] = +(line.id);
    }

    me.maskObjKeyCache[maskName] = result;
    return result;
  },

  prepareMask: function (mask) {
    var me = this;
    me.maskObjKeyCache[mask] || me.generateObjKeyLineMapping(mask);
  },

  addDataCollectedFromIdContainer: function (idContainer, instructions, allCollectedData, ofMask) {
    var me = this,
        mask,
        idsOfMask = idContainer[ofMask],
        idIsObjId = +(idsOfMask[0]) === +(idsOfMask[0]),
        requiredIdIsAvailableId = (instructions.idName === "id" && idIsObjId) || (instructions.idName === "guid" && (!idsOfMask[0].indexOf("(")));

    if (me.shouldUseDefaultDataCollection(instructions)) {
      me.logger.debug("Collecting data for all ids of mask", mask);
      if (instructions.objKeysRequired && (mask = (ofMask === "__nomask" ? undefined : ofMask))) {
        me.prepareMask(mask);
      }
      me.defaultDataCollection(allCollectedData, idsOfMask, instructions, mask);
    } else {
      if (instructions.anIdRequired) {
        instructions.onlyIdsInCollection = true; // this value persists for whole execution
        if (requiredIdIsAvailableId) {
          me.setUnchangedIdsAsResult(allCollectedData, idsOfMask);
        } else {
          me.getIdsOfIds(allCollectedData, idsOfMask, instructions);
        }
      }
    }
    return allCollectedData;
  },

  shouldUseDefaultDataCollection: function (instructions) {
    return instructions.objectFormatter
      || (!instructions.onlyIdsInCollection
        && instructions.converterConfig
        && instructions.formatterRequired);
  },

  processIdContainer: function (idContainer, instructions) {
    var me = this;
    return Object.keys(idContainer || {})
      .reduce(me.addDataCollectedFromIdContainer.bind(me, idContainer, instructions), []);
  },

  /**
   * @private
   * helpers for search
   */

  createObjKey: function (name, value) {
    var key = new ObjKey();
    key.name = name;
    key.data = [value];
    return key;
  },

  parseSearchValue: function (value) {
    var me = this;

    value = sol.common.ObjectUtils.isString(value)
      ? me.convertMultiIndex(value)
      : value;

    // multiIndexValue could be a simple string here as well
    return !Array.isArray(value)
      ? value
      : value.reduce(function (acc, val, i) {
        return me.addOR(acc + '"' + val + '"', i, value.length);
      }, "");
  },

  convertMultiIndex: function (value) {
    var me = this,
        values = value
          .split(me.pilcrow)
          .filter(function (str) {
          // filter out each broken multiIndex value like ¶¶Test
            return str.trim();
          });

    // return simple string when it is a single value because it is faster
    return values.length > 1 ? values : value;
  },

  extractSearchOpts: function (fields) {
    var me = this, opts = {}, objKeys = [];
    if (!Array.isArray(fields)) {
      throw "SordProvider: searchfields must be an array of objects!";
    }
    fields.forEach(function (field) {
      if ((typeof field !== "object") || (typeof field.key !== "string" && field.key) || ((typeof field.value !== "string") && (!Array.isArray(field.value)))) {
        throw "SordProvider: searchfield is no object, or key or value not suited for searching";
      }
      if (Array.isArray(field.value) && !field.value.length) {
        throw "SordProvider: " + field.key + ": it is not allowed to use empty arrays as a search value.";
      }
      if (field.key === "XDATEISO") {
        opts.xDateIso = field.value;
      } else if (field.key === "IDATEISO") {
        opts.iDateIso = field.value;
      } else if (field.key === "name") {
        opts.name = field.value;
      } else {
        objKeys.push(me.createObjKey(field.key, me.parseSearchValue(field.value)));
      }
    });
    if (!(objKeys.length || opts.xDateIso || opts.iDateIso || opts.name)) {
      throw "SordProvider: no search criteria defined!";
    }
    objKeys.length && (opts.objKeys = objKeys);
    return opts;
  },

  buildFindByIndex: function (masks, searchfields) {
    var me = this, findByIndex = new FindByIndex(), opts = me.extractSearchOpts(searchfields);

    opts.objKeys && (findByIndex.objKeys = opts.objKeys);
    opts.xDateIso && (findByIndex.XDateIso = opts.xDateIso);
    opts.iDateIso && (findByIndex.IDateIso = opts.iDateIso);
    opts.name && (findByIndex.name = opts.name);
    findByIndex.maskIds = masks;

    return findByIndex;
  },

  buildFindInfoForFindByIndex: function (masks, searchfields) {
    var me = this, findInfo = new FindInfo();
    me.logger.debug("Building findInfo for findByIndex");
    findInfo.findByIndex = me.buildFindByIndex(masks, searchfields);
    return findInfo;
  },

  buildFindInfoForFindDirect: function (masks, searchCriteria) {
    var me = this, findInfo = new FindInfo(), queryOpts;
    me.logger.debug("Building findInfo for search via mask(s) using findDirect", masks);

    sol.common.ObjectUtils.type(me.query, "string")
      && (queryOpts = { customQuery: String(me.query) });

    findInfo.findDirect = me.buildFindDirect(masks, searchCriteria, queryOpts);
    return findInfo;
  },

  getFindInfoBuilder: function () {
    var me = this;
    return (me.findDirect ? me.buildFindInfoForFindDirect : me.buildFindInfoForFindByIndex).bind(me);
  },

  /**
   * @private
   * helpers for finalizeCollector
   */

  generateMbsFromOptions: function (mbs) {
    var me = this;
    return mbs.map(function (mb) {
      return me.MBS[mb];
    });
  },

  createSordZ: function (mbs) {
    return (mbs.length || undefined)
      && mbs.reduce(function (sordZ, mb) {
        sordZ.add(mb);
        return sordZ;
      }, new SordZ());
  },

  addToOptimizationCache: function (cache, instructions) {
    var me = this;
    me.logger.debug("Storing instructions in optimization cache");
    return me[cache].push(instructions);
  },

  getFromOptimizationCache: function (cache, id) {
    var me = this;
    try {
      return me[cache][id - 1];
    } catch (e) {
      throw "SordProvider: id `" + id + "` not found in optimization cache.";
    }
  },

  getCachedInstructions: function (cacheName, optimization) {
    var me = this;
    return me.getFromOptimizationCache(cacheName + "OptimizationCache", optimization);
  },

  addInstructionsToCache: function (cacheName, instructions) {
    var me = this;
    return me.addToOptimizationCache(cacheName + "OptimizationCache", instructions);
  },

  /**
   * @private
   * helpers for parseOutputDefinition
   */

  addMb: function (mbs, type, key) {
    var me = this, uKey = key.toUpperCase(), mb = (
      ((type === "SORD") && (me.contains(me.BASICSORDKEYS, uKey) ? uKey : "MIN"))
      || ((type === "GRP") && type)
      || ""
    );
    return mb && !me.contains(mbs, mb) && mbs.push(mb);
  },

  addConverterKey: function (converterConfig, type, key, prop, converter) {
    var formatterTarget = this.SORDACCESSORS[type];
    if (!formatterTarget) {
      throw "SordProvider: type " + type + " not supported";
    }
    (type === "SORD" || type === "GRP")
      ? converterConfig[formatterTarget].push({ key: key, prop: prop, converter: converter })
      : converterConfig[formatterTarget][key] = prop; // map/blob definitions stored in objects!
  },

  cleanConfig: function (config) {
    return Object.keys(config).reduce(function (cleanedConfig, key) {
      if (Object.keys(config[key]).length) {
        cleanedConfig[key] = config[key];
      }
      return cleanedConfig;
    }, {});
  },

  formatterIsRequired: function (instructions) {
    var onlyOneMbAndItsNotForAnId = (instructions.mbs.length === 1 && !instructions.anIdRequired),
        multipleMbsRequired = instructions.mbs.length > 1;
    return !!(
      onlyOneMbAndItsNotForAnId // e.g. `DESC`, when only sord.desc is read
      || multipleMbsRequired
      || instructions.converterConfig.objKeys
      || instructions.converterConfig.mapKeys
      || instructions.converterConfig.formBlobs
    );
  },

  calcFlags: function (instructions) {
    var me = this, idRequired, guidRequired;
    me.logger.debug("Calculating flags for generated instructions.");

    idRequired = me.contains(instructions.mbs, "ID") && "id";
    guidRequired = me.contains(instructions.mbs, "GUID") && "guid";
    (instructions.anIdRequired = !!(idRequired || guidRequired))
      && me.logger.debug("Output will contain an objId or guid");

    if (instructions.objKeysRequired = me.contains(instructions.mbs, "GRP")) {
      me.logger.debug("Output will contain GRP-fields");
      ((!me.masks) || me.contains(me.masks, "__nomask"))
        && me.addMb(instructions.mbs, "SORD", "mask") // mask will be read from every sord
        && me.logger.debug("Since no mask was defined, `mask` has been added to SordZ of sord formatter");
    }

    instructions.formatterRequired = me.formatterIsRequired(instructions);
    me.logger.debug((instructions.formatterRequired ? "" : "No ") + "sord formatter will be used.");

    // determines which property will be read from sord when input>output is id>guid or guid>id
    if (instructions.formatterRequired) {
      (instructions.converterConfig.mapKeys || instructions.converterConfig.formBlobs)
        && me.addMb(instructions.mbs, "SORD", "id");
    } else {
      (instructions.idName = idRequired || guidRequired || "id")
        && me.logger.debug(["Using `{0}` as id property.", instructions.idName]);
    }
  },

  optimizeObjectAccessors: function (instructions) {
    var me = this;
    me.logger.debug("Optimizing sord object accessors");
    instructions.configKeys = Object.keys(instructions.converterConfig); // performance opt. for converterConfig
    instructions.mapAccessors = me.cleanConfig({ // performance opt. for keys in configKeys
      mapKeys: Object.keys(instructions.converterConfig.mapKeys || {}),
      formBlobs: Object.keys(instructions.converterConfig.formBlobs || {})
    });
    instructions.dvKeys = Object.keys(instructions.directValues); // performance opt. for directValues
  },

  assignFallbackIdProp: function (instructions, lonelyDef) {
    var me = this;
    function definesIdOrGuid(source) {
      return source.type === "SORD" && (source.key === "guid" || source.key === "id");
    }
    definesIdOrGuid(lonelyDef.source)
      && (instructions.fallbackIdProp = ((lonelyDef.target || {}).prop || me.fallbackIdProp));
  },

  addAsSourceTargetInstruction: function (instructions, source, target) {
    var me = this;
    me.logger.debug("Preparing sord formatter instructions for entry.");
    me.addMb(instructions.mbs, source.type, source.key);
    me.addConverterKey(instructions.converterConfig, source.type, source.key, target.prop, source.converter);
    target && target.prop && instructions.targetProps.push(target.prop);
  },

  addAsDirectValueInstruction: function (instanceScope, instructions, target) {
    var me = this;
    me.logger.debug("Instruction is a direct value.");
    instanceScope._directValuesDefined = true;
    instructions.directValues[target.prop] = target.value;
    instructions.targetProps.push(target.prop);
  },

  isValidSourceDefinition: function (sDef) {
    return typeof sDef === "object" && typeof sDef.type === "string" && typeof sDef.key === "string";
  },

  definesDirectValue: function (isValidTarget, target) {
    return isValidTarget && typeof target.prop === "string" && typeof target.value === "string";
  },

  addOutputInstruction: function (multipleOptsDefined, instructions, outputInstruction, i) {
    var me = this,
        source = outputInstruction.source, target = outputInstruction.target || {},
        validTargetDefinition = !!(target && target.prop);

    me.logger.debug("Processing output instruction " + (i + 1) + ":", outputInstruction);
    if (me.isValidSourceDefinition(source)) {
      if (multipleOptsDefined && !validTargetDefinition) {
        throw "SordProvider: Invalid instruction: each instruction must have a `target.prop`";
      }
      me.addAsSourceTargetInstruction(instructions, source, target);
    } else if (me.definesDirectValue(validTargetDefinition, target)) {
      me.addAsDirectValueInstruction(me, instructions, target);

    } else {
      throw "SordProvider: Invalid instruction: each instruction must have (`source.type`, `source.key` and `target.prop`) or (`target.prop` and `target.value`)";
    }
    return instructions;
  },

  parseOutputDefinition: function (outputDef) {
    var me = this,
        multipleOptsDefined = (outputDef.length > 1),
        instructions = {
          mbs: [], // used for generating a sordZ for CheckoutSord
          // defines, which values will be retrieved from the checked out sord
          targetProps: [], // used when sparse option is set
          converterConfig: { sordKeys: [], objKeys: [], mapKeys: {}, formBlobs: {} },
          directValues: {}
        };
    me.logger.debug("Parsing output definition.", outputDef);

    outputDef.reduce(me.addOutputInstruction.bind(me, multipleOptsDefined), instructions);

    !multipleOptsDefined
      && me._directValuesDefined // defined in addOutputInstruction
      && me.assignFallbackIdProp(instructions, outputDef[0]);

    instructions.converterConfig = me.cleanConfig(instructions.converterConfig);
    me.calcFlags(instructions); // precalculate flags to save time in loops later on
    me.optimizeObjectAccessors(instructions); // cache accessors (Object.keys()) in instructions

    return instructions;
  },

  finalizeCollector: function (instructions) {
    var me = this, mbs,
        onlyOneMbAndItsForAnId = (instructions.mbs.length === 1 && instructions.anIdRequired);

    // only generate a sordZ if more than the ID is to be included in the output
    if (onlyOneMbAndItsForAnId && (instructions.converterConfig && (!instructions.converterConfig.mapKeys && !instructions.converterConfig.formBlobs))) {
      me.logger.debug("No sord formatter will be used");
      instructions.converterConfig = undefined;
      instructions.sordZ = undefined;
      instructions.formatterRequired = false;
    } else {
      // update instruction.mbs array here
      mbs = me.generateMbsFromOptions(instructions.mbs);
      instructions.sordZ = me.createSordZ(mbs);
    }
  },

  idsOf: function (findResult) {
    return (findResult && findResult.ids && Array.prototype.slice.call(findResult.ids)) || [];
  },

  impureConcat: function (target, source) {
    for (var i = 0, len = source.length; i < len; i++) {
      target.push(source[i]);
    }
  },

  updateSearchCache: function (findResult, desiredResults) {
    var me = this,
        cache = (me.searchCache[findResult.searchId] || (me.searchCache[findResult.searchId] = {})),
        results = (findResult.ids && findResult.ids.length) || 0;
    me.logger.debug("Updating search cache");
    cache.idx = cache.idx ? cache.idx + results : results;
    cache.desiredResults = cache.desiredResults ? (cache.desiredResults - results) : (desiredResults - results);
  },

  getFromSearchCache: function (store, searchId) {
    var me = this, cache = me.searchCache[searchId] || {};
    me.logger.debug("Testing cache for search information");
    store.idx = cache.idx;
    store.desiredResults = cache.desiredResults;
  },

  closeFind: function (searchId) {
    var me = this;
    me.searchCache[searchId] = undefined;
    ixConnect.ix().findClose(searchId);
  },

  maintainSearchInfo: function (searchInfo, findResult, maxResults) {
    var me = this;
    if (findResult.moreResults) {
      me.updateSearchCache(findResult, maxResults);
      me.getFromSearchCache(searchInfo, findResult.searchId);
      findResult.moreResults = searchInfo.desiredResults > 0 ? findResult.moreResults : false;
      me.logger.debug(
        ["Found `{1}` results so far, desired remaining results `{1}` "],
        searchInfo.idx, searchInfo.desiredResults
      );
    }
  },

  prepareSearchInfo: function (searchInfo, searchId, maxResults, pageSize) {
    var me = this, desiredResults;
    me.getFromSearchCache(searchInfo, searchId);
    pageSize || (pageSize = me.pageSizeMax);
    desiredResults = searchInfo.desiredResults || maxResults;
    searchInfo.maxSords = (desiredResults <= pageSize ? desiredResults : pageSize); // limit to pagesize

    me.logger.debug([
      "Find by searchId `{0}`, desired result count `{1}`, results so far `{2}`, max results per search `{3}` ",
      (searchId || "N/A (new search)"), searchInfo.desiredResults, searchInfo.idx, searchInfo.maxSords
    ]);
  },

  find: function (findInfo, maxResults, pageSize, sordZ, searchId) {
    var me = this, searchInfo = {}, findResult;

    me.prepareSearchInfo(searchInfo, searchId, maxResults, pageSize);

    if (me.manualPagingFrom === undefined || me.paging === true) {
      findResult = (searchInfo.idx === undefined)
        ? ixConnect.ix().findFirstSords(findInfo, searchInfo.maxSords, sordZ)
        : ixConnect.ix().findNextSords(searchId, searchInfo.idx, searchInfo.maxSords, sordZ);

      me.maintainSearchInfo(searchInfo, findResult, maxResults);

      me.moreResults = !!findResult.moreResults;
      findResult.moreResults
        ? me.logger.debug("More results available. Id: " + findResult.searchId)
        : me.closeFind(String(findResult.searchId));
    } else {
      findResult = ixConnect.ix().findNextSords(searchId, me.manualPagingFrom, searchInfo.maxSords, sordZ);
      findResult.moreResults && (me.moreResults = true);
    }

    return findResult;
  },


  processResult: function (findResult, ids) {
    var result = { ids: ids };
    if (findResult.moreResults) {
      result.paging = String(findResult.searchId);
    }
    return result;
  },

  findAll: function (findInfo, maxResults, sordZ) {
    var me = this, ids = [],
        findResult = { moreResults: true };

    while (findResult.moreResults) {
      findResult = me.find(findInfo, maxResults, undefined, sordZ, findResult.searchId);
      me.impureConcat(ids, me.idsOf(findResult));
    }

    me.logger.debug(["Found {0} results", ids.length]);

    return me.processResult(findResult, ids);
  },

  pageFind: function (findInfo, maxResults, pageSize, sordZ, searchId) {
    var me = this, findResult;

    me.logger.debug("Executing find using paging.");
    me.logger.debug((searchId ? "Continuing" : "Starting new") + " search");
    findResult = me.find(findInfo, maxResults, pageSize, sordZ, searchId);
    return me.processResult(findResult, me.idsOf(findResult));
  },

  findIds: function (masks, searchFields, options) {
    var me = this, paging = options.paging,
        maxResults = (options.maxResults || me.defaultMaxResults),
        pageSize = options.pageSize, sordZ = options.idSordZ,
        result;

    if (options.maxResults <= 0) {
      throw "SordProvider: `options.maxResults` must be greater than 0";
    }
    if (options.pageSize <= 0) {
      throw "SordProvider: `options.pageSize` must be greater than 0";
    }

    if (paging) {
      result = (typeof paging === "boolean") // initialize paging
        ? me.pageFind((me.getFindInfoBuilder())(masks, searchFields), maxResults, pageSize, sordZ)
        : me.pageFind((me.manualPagingFrom === undefined ? undefined : (me.getFindInfoBuilder())(masks, searchFields)), maxResults, pageSize, sordZ, paging); // continue paging search
    } else {
      me.logger.debug("Using standard search");
      result = me.findAll((me.getFindInfoBuilder())(masks, searchFields), maxResults, sordZ);
    }

    return result;
  },

  searchFor: function (infos, searchCriteria, options, masks) {
    var me = this, result, ids, searchResults = { idsByMask: {} };

    result = me.findIds(masks, searchCriteria, options);

    if ((ids = result.ids) && me.manualPagingFrom === undefined) {
      infos.fetched += ids.length;
    }

    ids.length
      && (infos.found = true)
      && (searchResults.idsByMask[((masks.length === 1) && masks[0]) ? masks[0] : "__nomask"] = ids);

    searchResults.paging = result.paging;

    return searchResults;
  },

  searchViaIndex: function (masks, searchCriteria, options) {
    var me = this, infos = {}, result;

    me.logger.debug("... find by index search using search criteria", searchCriteria);

    if (!me.pagingIdDefined(options.paging)) {
      if (!Array.isArray(masks)) {
        if (options.allowEmptyMask === true) {
          me.masks = (masks = [""]);
        } else {
          throw "SordProvider: masks must be an array of strings!";
        }
      }
    }

    infos.fetched = 0;

    result = me.searchFor(infos, searchCriteria, options, masks);

    infos.pagingId && infos.found && !result.paging && me.endPaging(infos.pagingId);

    return infos.found
      ? (
        me.logger.debug("Search had " + infos.fetched + " results. " + (result.paging ? "Paging active: " + result.paging : "")),
        result
      )
      : (me.logger.debug("No search results"), undefined);
  },

  addDirectValuesToSords: function (sords, instructions) {
    var i = sords.length, dvKeys = instructions.dvKeys,
        k, kLen = dvKeys.length, directValues = instructions.directValues,
        sord, prop;

    while (i--) {
      sord = sords[i];
      k = kLen;
      while (k--) {
        prop = dvKeys[k];
        sord[prop] = directValues[prop];
      }
    }
    return sords;
  },

  addDirectValuesToIds: function (ids, instructions) {
    var fallbackIdProp = instructions.fallbackIdProp, i, iLen = ids.length, dvKeys = instructions.dvKeys,
        k, kLen = dvKeys.length, directValues = instructions.directValues,
        sord, prop, sords = [];

    for (i = 0; i < iLen; i++) {
      sord = {};
      sord[fallbackIdProp] = ids[i];
      k = kLen;
      while (k--) {
        prop = dvKeys[k];
        sord[prop] = directValues[prop];
      }
      sords.push(sord);
    }
    return sords;
  },

  buildIdContainer: function (inputIds) {
    var me = this, idContainer = { __nomask: [] };

    inputIds || (inputIds = {});

    if (Array.isArray(inputIds)) {
      me.logger.debug("Processing ids passed via parameter. No search preceded.");
      idContainer.__nomask = inputIds;
    } else if (typeof inputIds.idsByMask === "object") {
      me.logger.debug("Processing ids collected via search.");
      idContainer = inputIds.idsByMask;
    } else {
      me.logger.debug("Processing a single id. No search preceded.");
      idContainer.__nomask.push(inputIds); // single id from parameter
    }

    return idContainer;
  },

  processIds: function (inputIds, instructions) {
    var me = this,
        sords,
        idContainer;

    idContainer = me.buildIdContainer(inputIds);
    me.finalizeCollector(instructions);
    sords = me.processIdContainer(idContainer, instructions);

    return (instructions.dvKeys.length)
      ? (
        (instructions.onlyIdsInCollection
          ? me.addDirectValuesToIds
          : me.addDirectValuesToSords
        )(sords, instructions))
      : sords;
  },

  removeEmptyFields: function (sords, props) {
    var i = sords.length, k, kLen = props.length, sord, prop;
    if (!kLen) {
      return; // no props to check
    }
    while (i--) {
      sord = sords[i];
      k = kLen;
      while (k--) {
        (sord[(prop = props[k])] === "") && (sord[prop] = undefined);
      }
    }
  },

  propsToArrays: function (sords, props) {
    var me = this, i = sords.length, k, kLen = props.length, sord, prop, arr,
        pilcrow = me.pilcrow;
    if (!kLen) {
      return; // no props to check
    }
    while (i--) {
      sord = sords[i];
      k = kLen;
      while (k--) {
        arr = (sord[(prop = props[k])] = (sord[prop] || "").split(pilcrow));
        (arr.length === 1 && arr[0] === "") && (sord[prop] = []);
      }
    }
  },

  fastFilter: function (sords, filter) {
    var i = 0, iLen = sords.length, curSord, prop, k, kLen = filter.length, curFil,
        match, filtered = [];
    for (; i < iLen; i++) {
      curSord = sords[i];
      k = 0;
      while (k < kLen && (match = (curFil = filter[k]).value.test(curSord[(prop = curFil.prop)]))) {
        k++, curFil.exclude && (curSord[prop] = undefined);
      }
      match && filtered.push(curSord);
    }
    return filtered;
  },

  templateSordFilter: function (sords, filter) {
    var i = 0, iLen = sords.length, curSord, k, kLen = filter.length, curFil,
        match, filtered = [];
    for (; i < iLen; i++) {
      curSord = sords[i];
      k = 0;
      while (k < kLen && (match = (curFil = filter[k]).value.test(sol.common.ObjectUtils.getProp(curSord, curFil.prop)))) {
        k++; // exclude does not work in templateSord mode
      }
      match && filtered.push(curSord);
    }
    return filtered;
  },

  filterSords: function (sords, filter) {
    var me = this;
    return me.formatAsTemplateSord
      ? me.templateSordFilter(sords, filter)
      : me.fastFilter(sords, filter);
  },

  arrayToRegExp: function (arr, wc) {
    var compl, len = arr.length,
        addBitwiseOR = function (query, index, length) {
          return ((index + 1 < length) && (query += "|")), query;
        };

    compl = arr.reduce(function (acc, str, i) {
      if (typeof str !== "string") {
        throw "SordProvider: only string elements are allowed for filter criteria arrays";
      }
      return acc + addBitwiseOR("^" + str + "$", i, len);
    }, "");

    return new RegExp(compl.replace(new RegExp("\\" + wc, "g"), "." + wc));
  },


  stringToRegExp: function (str, wc, ignoreCase) {
    return new RegExp("^" + str.replace(new RegExp("\\" + wc, "g"), "." + wc), (ignoreCase ? "i" : ""));
  },

  contains: function (val, s) {
    return !!~val.indexOf(s);
  },

  buildValueQuery: function (key, value, tokenized) {
    var me = this;
    return me.FIND_DIRECT[tokenized ? "FIELD_OBJ_KEY_TOKENIZED" : "FIELD_OBJ_KEY"]
      + key
      + ": "
      + (!me.contains(value, '"') ? '"' + value + '"' : value); // LINE_MYFIELDNAME: "My value"
  },

  /* This is a workaround. getContextTerms does not support wildcards.
   * Therefore before executing the actual search, all available
   * values for the grouping term are collected and then filtered
   * for the defined value, which can contain a wildcard. */
  getAvailableTerms: function (key, value, ignoreCase, findInfo, maxGroups) {
    var me = this, re = me.stringToRegExp(value, me.wildCard, ignoreCase);

    // search for terms of the field `key`
    return me.getContextTerms(
      findInfo,
      me.getGroupingTerm({ groupBy: { type: "GRP", key: key } }),
      (+(maxGroups) || me.defaultMaxGroups)
    )
      .filter(function (result) { // return only terms containing the wildcard value
        return re.test(String(result.term));
      });
  },

  valuesWithoutWildcard: function (criterion) {
    var me = this;
    return !Array.isArray(criterion.value)
      ? !me.contains(criterion.value, me.wildCard) //str
      : !criterion.value.some(function (val) {
        return me.contains(val, me.wildCard);
      });
  },

  getFindInfoForConstantSearchValues: function (masks, searchCriteria, maxGroups) {
    var me = this, constantCriteria = searchCriteria.filter(me.valuesWithoutWildcard.bind(me));

    if (constantCriteria.length !== searchCriteria.length) {
      me.logger.debug("BEGIN: Building supplementary findInfo: searchcriteria contained wildcards");
      return me.buildFindInfoForContextTerms(masks, constantCriteria, maxGroups); //one time recursive call! uses all criteria without wildCards
    }
  },

  addOR: function (query, index, length) {
    return ((index + 1 < length) && (query += " OR ")), query;
  },

  getCriterionQuery: function (key, value, ignoreCase, tokenized, constantCriteriaFindInfo, maxGroups, params) {
    var me = this,
        addTermsToCriterionQuery = function (criterionQuery, term, i, availableTerms) {
          var resTerm = String(term.term);
          (!me.contains(resTerm, me.wildCard))
          && (criterionQuery += me.parens(
            me.getCriterionQuery(
              key,
              me.sanitizeQueryValue(resTerm, params),
              ignoreCase,
              tokenized))); // recursion!
          return me.addOR(criterionQuery, i, availableTerms.length);
        };

    return !me.contains(value, me.wildCard)
      ? me.buildValueQuery(key, value, tokenized)
      : (
        (me.getAvailableTerms(key, value, ignoreCase, constantCriteriaFindInfo, maxGroups)
          .reduce(addTermsToCriterionQuery, ""))
        || me.buildValueQuery(key, value.replace(/\*/g, ""), tokenized)
      );
  },

  addCriterionToQuery: function (maxGroups, supplementaryFindInfo, query, criterion) {
    var me = this,
        criterionQuery,
        addCriterionQueriesForAllValuesToQuery = function (valuePartOfQuery, val, i, values) {
          valuePartOfQuery += me.parens(
            me.getCriterionQuery(
              criterion.key,
              me.sanitizeQueryValue(val, criterion),
              (criterion.ignoreCase === true),
              (criterion.tokenized === true),
              supplementaryFindInfo,
              maxGroups,
              criterion
            ).trim()
          );
          return me.addOR(valuePartOfQuery, i, values.length);
        };

    me.logger.debug("Criterion", criterion);
    if (Array.isArray(criterion.value) && !criterion.value.length) {
      throw "SordProvider: " + criterion.key + ": it is not allowed to use empty arrays as a search value.";
    }
    criterionQuery = (Array.isArray(criterion.value) ? criterion.value : [criterion.value])
      .reduce(addCriterionQueriesForAllValuesToQuery, "");

    criterionQuery = Array.isArray(criterion.value) ? me.parens(criterionQuery) : criterionQuery;
    me.logger.debug("Generated query for criterion:", criterionQuery);
    return query += criterionQuery;
  },

  getSearchCriteriaQuery: function (searchCriteria, maxGroups, supplementaryFindInfo) {
    var me = this;
    me.logger.debug("Building query for search criteria");
    return searchCriteria.reduce(me.addCriterionToQuery.bind(me, maxGroups, supplementaryFindInfo), "");
  },

  parens: function (s) {
    return " (" + s + ") ";
  },

  buildRegEx: function (values) {
    return new RegExp(values.join("|"), "gi");
  },

  buildMasksQuery: function (masks) {
    var me = this;
    return masks.reduce(function (masksQuery, m, i) {
      return me.addOR(
        (masksQuery + me.parens(me.FIND_DIRECT.FIELD_MASK_NAME + ':"' + me.sanitizeQueryValue(m) + '"')),
        i,
        masks.length
      );
    }, "");
  },

  escapeOrRemoveInvalidCharacters: function (val, params) {
    var me = this,
        result = val,
        valueOrOtherwise = function (value, otherwise) {
          return typeof value == 'undefined' ? otherwise : value;
        },
        sanitizeParams = (params || {}).sanitize || {};

    sanitizeParams.escapeValues = valueOrOtherwise(sanitizeParams.escapeValues, true);
    sanitizeParams.blankValues = valueOrOtherwise(sanitizeParams.blankValues, true);
    sanitizeParams.removeDupWhiteSpaces = valueOrOtherwise(sanitizeParams.removeDupWhiteSpaces, true);

    if (sanitizeParams.escapeValues) {
      result = result.replace(me.escapeRegEx || (me.escapeRegEx = me.buildRegEx(me.ESCAPE_VALUES)), "\\$1");
    }
    if (sanitizeParams.blankValues) {
      result = result.replace(me.removeRegEx || (me.removeRegEx = me.buildRegEx(me.BLANK_VALUES)), " ");
    }
    if (sanitizeParams.removeDupWhiteSpaces) {
      // remove probably invalid double/tripple whitespaces
      result = result.replace(me.removeDupWhiteSpaces || (me.removeDupWhiteSpaces = /\s\s+/g), " ");
    }

    result = result.trim();

    return result;
  },

  sanitizeQueryValue: function (val, params) {
    var me = this;
    return me.escapeOrRemoveInvalidCharacters(val, params); //TODO: filterStopWords like in BS-Knowledge?
  },

  extendQueryByCustomQuery: function (query, custom) {
    var me = this;
    return me.parens(me.sanitizeQueryValue(custom)) + (query ? me.parens(query) : "");
  },

  extendQueryByWildCard: function (query) {
    var me = this;
    return me.parens(me.wildCard) + me.parens(query);
  },

  buildQuery: function (masks, searchCriteria, queryOpts) {
    var me = this, query = "", supplementaryFindInfo;

    me.logger.debug("Building search query");
    me.FIND_DIRECT = ixConnect.getCONST().FIND_DIRECT;

    if (!(masks.length === 1 && masks[0] === "")) {
      me.logger.debug("Including masks in query", masks);
      query += me.parens(me.buildMasksQuery(masks));
    }

    // this will be used to find all possible values of a criterion if a criterion's search value contains the wildcard
    (supplementaryFindInfo = me.getFindInfoForConstantSearchValues(masks, searchCriteria, queryOpts.maxGroups))
      && me.logger.debug("END: Built supplementary findInfo");

    query += me.getSearchCriteriaQuery(searchCriteria, queryOpts.maxGroups, supplementaryFindInfo);

    queryOpts.customQuery
      && (query = me.extendQueryByCustomQuery(query, queryOpts.customQuery));

    me.logger.debug("Built query", query);

    return query
      ? (queryOpts.customQuery ? query : me.extendQueryByWildCard(query))
      : me.wildCard;
  },

  buildFindDirect: function (masks, searchCriteria, queryOpts) {
    var me = this, findDirect = new FindDirect();
    me.logger.debug("Building findDirect");
    queryOpts || (queryOpts = {});
    findDirect.query = me.buildQuery(masks, searchCriteria, queryOpts).replace(/\s\s+/g, " ");
    findDirect.searchInIndex = true;
    if (queryOpts.customQuery) {
      findDirect.searchInMemo = true;
      findDirect.searchInFulltext = true;
      findDirect.searchInSordName = true;
    }
    return findDirect;
  },

  buildFindInfoForContextTerms: function (masks, searchCriteria, queryOpts) {
    var me = this, findInfo = new FindInfo();
    me.logger.debug("Building findInfo for search via mask(s)", masks);

    queryOpts || (queryOpts = {});
    sol.common.ObjectUtils.type(me.query, "string")
      && (queryOpts.customQuery = String(me.query));

    findInfo.findDirect = me.buildFindDirect(masks, searchCriteria, queryOpts);
    return findInfo;
  },

  getGroupingTerm: function (fuzzy) {
    if ((fuzzy.groupBy || {}).type !== "GRP") {
      throw "SordProvider: Fuzzy Type `" + fuzzy.groupBy.type + "` not implemented. Supported: GRP";
    }
    return (fuzzy.groupBy.tokenized === true ? "tLINE." : "LINE.") + fuzzy.groupBy.key;
  },

  getContextTerms: function (findInfo, groupingTerm, maxGroups) {
    var me = this;
    me.logger.debug(["Context search term `{0}`, max results `{1}`", groupingTerm, maxGroups]);
    return Array.prototype.slice.call(ixConnect.ix().getContextTerms(findInfo, groupingTerm, maxGroups));
  },

  castThenAddResults: function (acc, result) {
    acc[result.term] = +(result.docNum);
    return acc;
  },

  searchViaContextTerms: function (masks, searchCriteria, options) {
    var me = this, maxGroups = options.fuzzy.maxGroups;

    me.logger.debug("... context term search using search criteria", searchCriteria);
    return me.getContextTerms(
      me.buildFindInfoForContextTerms(masks, searchCriteria, { maxGroups: maxGroups }),
      me.getGroupingTerm(options.fuzzy),
      (+(maxGroups) || me.defaultMaxGroups)
    )
      .reduce(me.castThenAddResults, {});
  },

  performSearch: function (masks, searchCriteria, options) {
    var me = this;
    me.logger.debug("Starting search process using defined options");
    return (
      (options.fuzzy ? me.searchViaContextTerms : me.searchViaIndex).bind(me)
    )(masks, searchCriteria, options);
  },

  optimizationIdDefined: function (optimization) {
    return typeof optimization === "number";
  },

  pagingIdDefined: function (pagingId) {
    return typeof pagingId === "string";
  },

  initializeCaching: function (initOptimizationCache, instructions) {
    var me = this;
    if (initOptimizationCache) {
      me._initialOptimization = me.addInstructionsToCache("manual", instructions);
    }
  },

  addFilterDefinition: function (instructions, filter) {
    var me = this;
    if (!Array.isArray(filter)) {
      throw "SordProvider: filter must be an array of objects!";
    }

    filter.forEach(function (criterion) {
      if ((typeof criterion !== "object") || (typeof criterion.prop !== "string" && criterion.prop) || ((typeof criterion.value !== "string") && (!Array.isArray(criterion.value)))) {
        throw "SordProvider: filter criterion is no object, or prop or value not suited for filtering";
      }
      criterion.value = (typeof criterion.value === "string" ? me.stringToRegExp : me.arrayToRegExp)(criterion.value, me.wildCard);
    });

    instructions.filter = filter;
  },

  generateInstructions: function (output, filter, options) {
    var me = this, instructions;

    if (Array.isArray(output) && output.length > 0) {
      instructions = me.parseOutputDefinition(output);
      filter && me.addFilterDefinition(instructions, filter);
      // add filter to instructions
      me.logger.debug("Output options parsed. Generated optimized instructions:", instructions);
      me.initializeCaching((me.optimize === true), instructions);
    } else {
      if (output.formatter && output.config && !options.fuzzy) {
        instructions = {
          mbs: [], // used for generating a sordZ for CheckoutSord
          // defines, which values will be retrieved from the checked out sord
          targetProps: [], // used when sparse option is set
          converterConfig: { sordKeys: [], objKeys: [], mapKeys: {}, formBlobs: {} },
          directValues: {},
          dvKeys: [],
          objectFormatter: {
            formatter: output.formatter,
            config: output.config
          }
        };

        filter && me.addFilterDefinition(instructions, filter);
      } else if (!options.fuzzy) {
        throw "SordProvider: output parameter must be defined as an Array or as Object with parameter formatter and config to use ObjectFormatter";
      }
      me.logger.debug("Output schema will be produced by fuzzy search (getContextTerms)");
    }
    return instructions;
  },


  getInstructionsFromCache: function () {
    var me = this, instructions, optimizationId = me.optimize;
    if (me.optimizationIdDefined(optimizationId)) {
      instructions = me.getCachedInstructions("manual", optimizationId);
      me.logger.debug("Optimized Run: Using cached instructions instead of parsing output parameter");
    }
    return instructions;
  },

  initSearchAndProcessingInstructions: function (outputDefinition, filter, options, result) {
    var me = this, instructions;
    !(instructions = me.getInstructionsFromCache())
      && (instructions = me.generateInstructions(outputDefinition, filter, options))
      && me._initialOptimization // value has been set in generateInstructions, may be undefined if optimization is not active
      && (result.optimization = me._initialOptimization);

    options.paging // restrict pageSize to max allowed value
      && (options.pageSize > me.pageSizeMax)
      && (options.pageSize = me.pageSizeMax);

    options.idSordZ = (instructions.idName === "guid") ? SordC.mbOnlyGuid : SordC.mbOnlyId;

    me.formatAsTemplateSord = options.formatAsTemplateSord === true;
    me.ipn = options.ignorePropertyNames === true || (me.formatAsTemplateSord && options.ignorePropertyNames === undefined);

    return instructions;
  },

  /**
   * @return {Object} return
   * @return {String[]|Object[]} return.sords all found and formatted sords. String[] if `output` has only one property or Object[] otherwise
   * @return {Object} return.groups map of the group by field values and their result counts
   * @return {String} return.paging the searchId which can be again passed to this service as `options.paging` parameter to continue the search
   * @return {Number} return.optimization the optimizationId which can be again passed to this service as `optimize`
   */
  process: function () {
    var me = this, processingInstructions, deliverables, result = {},
        outputDefinition = me.output, options = (me.options || {}),
        filter = me.filter, fuzzySearch = !!options.fuzzy,
        pagingOpt = (options.paging || (options.paging = options.searchId));

    if (me.ids && me.id) {
      throw "SordProvider: you can only pass the `ids` or the `id` parameter, but not both at the same time.";
    }

    if ((options.endPaging === true)) {
      if (!me.pagingIdDefined(pagingOpt)) {
        throw "SordProvider: `endPaging:true` has been defined. However, no searchId was passed.";
      }
      me.closeFind(pagingOpt);
      return { sords: [] };
    }

    if (options.startPagingFrom !== undefined) {
      me.manualPagingFrom = options.startPagingFrom;
    }

    if (sol.common.ObjectUtils.type(options.query, "string")) {
      me.query = options.query;
    }

    if (options.findDirect || me.query) { // will use findDirect instead of findByIndex
      me.findDirect = true;
    }

    !fuzzySearch && // no preparations required for a fuzzy search
      (processingInstructions = me.initSearchAndProcessingInstructions(outputDefinition, filter, options, result));

    deliverables = (me.search || me.pagingIdDefined(pagingOpt))
      ? me.performSearch(me.masks, me.search, options) // findByIndex or getContextTerms (fuzzy) search
      : (me.id || me.ids); // use specified ids instead of searching

    if (fuzzySearch) { // only stores results. no processing for fuzzy search results
      me.logger.debug("Returning fuzzy search results (groups)");
      result.groups = deliverables;
    } else {
      result.sords = [];
      deliverables
        && (result.sords = me.processIds(deliverables, processingInstructions)) // apply output rules
        && deliverables.paging // if the search was paged and can be continued ...
        && (result.searchId = deliverables.paging); // ... store pagingId in result

      me.moreResults && (result.moreResults = true);

      if (result.sords && typeof result.sords[0] === "object") {
        processingInstructions.filter
          && (result.sords = me.filterSords(result.sords, processingInstructions.filter));

        options.sparse
          && me.removeEmptyFields(result.sords, processingInstructions.targetProps);

        Array.isArray(options.propsAsArrays)
          && me.propsToArrays(result.sords, options.propsAsArrays);
      }
    }

    return result;
  }
});


/**
 * This class contains util functions to facilitate working with the SordProvider.
 * E.g. run, which takes a configuration and returns processed sords.
 * runOptimized which takes a configuration, a cacheObject (this should be a persistent
 * object in your class) and a optimization name. Additionally to the processed sords,
 * an "optimization" id will be returned. This id will automatically get stored at
 * cacheObject[optimizationName].
 *
 * The passed config will be stringified and parsed to create a copy of it. This reduces
 * bugs. If you already pass a copied configuration into this function, you can disable
 * the copy mechanism by defining { copy: false } as the fourth parameter.
 * @elojc
 * @eloas
 * @eloix
 */
sol.define("sol.common.SordProviderUtils", {
  singleton: true,

  runOptimized: function (config, cacheObject, optimizationName, opts) {
    var me = this, result;
    opts || (opts = {});
    try {
      config = config ? (opts.copy === false ? config : JSON.parse(JSON.stringify(config))) : {};
    } catch (e) {
      throw "SordProviderUtils: configuration object could not be converted to JSON. Do not pass java objects/strings to SordProvider! " + e;
    }

    if (typeof cacheObject !== "object") {
      throw "SordProviderUtils: the second argument to runOptimized must be a persistent object. Optimization ids will be stored here.";
    }

    if (!optimizationName) {
      throw "SordProviderUtils: the third argument to runOptimized must be a unique string (name) for the optimization. The name will be used for referencing a stored optimization id.";
    }

    if (config.optimize = cacheObject[optimizationName]) { // exclude unnecessary properties from config
      config["output"] = undefined;
      config["filter"] = undefined;
    } else {
      config.optimize = true;
    }

    cacheObject[optimizationName] = (result = me.run(config, { copy: false })).optimization; // store optimization ID in cache
    return result;
  },

  run: function (config, opts) {
    opts || (opts = {});
    try {
      config = config ? (opts.copy === false ? config : JSON.parse(JSON.stringify(config))) : {};
    } catch (e) {
      throw "SordProviderUtils: configuration object could not be converted to JSON. Do not pass java objects/strings to SordProvider!" + e;
    }
    return (sol.create("sol.common.SordProvider", config)).process(config || {});
  },

  create: function (config) {
    return sol.create("sol.common.SordProviderBuilder", config || {});
  }

});

/**
 * This class is in experimental mode and is not allowed to used from external source.
 *
 * Only for internal purposes currently.
 * @experimental
 * @private
 */
sol.define("sol.common.SordProviderBuilder", {

  _DEFAULT_FORMATTER: "sol.common.ObjectFormatter.TemplateSord",

  initialize: function (config) {
    var me = this;
    me.ids = {};
    me.masks = [];
    me.search = {};
    me.output = {};
    me.options = config.options || {};


    me.$super("sol.Base", "initialize", [config]);
    return me;
  },

  /**
   *
   * @param {*} masks
   * @returns
   */
  addMasks: function (masks) {
    var me = this;

    sol.common.ObjectUtils
      .toArray(masks)
      .filter(function (mask) {
        return sol.common.ObjectUtils.isString(mask);
      })
      .forEach(function (mask) {
        // add mask only if the mask not already exists.
        me.masks.indexOf(mask) == -1 && me.masks.push(mask);
      });

    return me;
  },

  useId: function (id) {
    var me = this;

    if (!sol.common.RepoUtils.isRepoId(id)) {
      throw Error("`id`=" + id + " is not a valid elo object identifier. See checkoutSord documentation");
    }

    me.ids[id] = true;
    return me;
  },

  useIds: function (ids) {
    var me = this;
    sol.common.ObjectUtils.toArray(ids)
      .forEach(function (id) {
        me.useId(id);
      });

    return me;
  },

  /**
   * @param type {String}
   */
  addSearchCriteria: function (key, values, type) {
    var me = this;

    if (!me.search[key]) {
      me.search[key] = {
        values: sol.common.ObjectUtils.toArray(values),
        type: type
      };
    } else {
      // already exist, append new values
      sol.common.ObjectUtils
        .toArray(values)
        .forEach(function (val) {
          me.search[key].values.push(val);
        });
    }
    return me;
  },

  addDateSearchCriteria: function (key, value) {
    var me = this;
    me.addSearchCriteria(key, value, "date");
    return me;
  },

  addDateRangeCriteria: function (key, start, end) {
    var me = this,
        rangeStr;

    rangeStr = (start || "") + "..." + (end || "");
    me.addSearchCriteria(key, rangeStr, "date");
    return me;
  },

  addOutput: function (source, target) {
    var me = this, outputDef, outputIdentifier;

    if (!(source.type || source.key)) {
      throw Error("source must have structure of {type, key}");
    }

    outputIdentifier = source.type + source.key;

    if (!me.output[outputIdentifier]) {
      me.output[outputIdentifier] = { source: source, target: target };
    } else {
      outputDef = me.output[outputIdentifier];

      if (outputDef.target.prop !== target.prop) {
        // ignore output if it is the same...
        // If it is not equals we have two fields with different names
        // that should not be possible
        throw Error("Output definition already exists for " + JSON.stringify(source) + " `target.prop` may not be different here");
      }
    }

    return me;
  },

  addOutputs: function (outputs) {
    var me = this;
    (outputs || []).forEach(function (output) {
      me.addOutput(output.source, output.target);
    });
    return me;
  },

  get: function () {
    // hide internal date so we want to copy relevant config data
    var me = this, config = {};

    if (!sol.common.ObjectUtils.isEmpty(me.ids)) {
      config.ids = me.mapIds();
    } else {
      config.masks = me.masks;
      config.search = me.mapToSearchCriteria();
    }

    config.output = me.mapOutput();
    config.options = me.options;

    return config;
  },

  run: function (options) {
    var me = this;

    return (me.optimizationName && me.optimizationCache)
      ? sol.common.SordProviderUtils.runOptimized(me.get(), me.optimizationCache, me.optimizationName, options)
      : sol.common.SordProviderUtils.run(me.get(), options);
  },

  mapToSearchCriteria: function () {
    var me = this;
    return Object.keys(me.search)
      .map(function (key) {
        var searchCriteria = me.search[key],
            searchCriteriaObj = { key: key, value: searchCriteria.values };
        searchCriteria.type && (searchCriteriaObj.type = searchCriteria.type);
        return searchCriteriaObj;
      });
  },

  mapOutput: function () {
    var me = this;
    if (sol.common.ObjectUtils.isEmpty(me.output)) {
      return {
        formatter: me._DEFAULT_FORMATTER,
        config: {}
      };
    } else {
      return Object.keys(me.output)
        .map(function (key) {
          return me.output[key];
        });
    }
  },

  mapIds: function () {
    var me = this;
    return Object.keys(me.ids)
      .filter(function (id) {
        return !!me.ids[id];
      })
      .map(function (id) {
        return id;
      });
  }

});