Skip to content
Snippets Groups Projects
process.js 69.79 KiB
import("ActivityTask_lib");
import("KeywordRegistry_basic");
import("system.translate");
import("system.datetime");
import("JditoFilter_lib");
import("system.process");
import("system.util");
import("system.vars");
import("system.net");
import("system.logging");
import("system.db");
import("system.entities");
import("Sql_lib");
import("system.indexsearch");

/**
 * Methods for duplicate scanning.
 * Do not create an instance of this!
 * 
 * @class
 */
function DuplicateScannerUtils() {}

/*
 * Loads all prefilters for a scanner in the form of arrays in an array.
 * Single filter record: [FILTER_CONDITION, COUNT_CHARACTERS_TO_USE, MAX_RESULTS_THRESHOLD]
 *
 *  @param {String} pFilterName Name of the filter
 *  @param {String} pTargetEntity Entity which has been configured
 *  @returns {String[[]]} Array of arrays containing the configured values
 */
DuplicateScannerUtils.loadFilters = function(pFilterName, pTargetEntity)
{
    let query = "select FILTER_CONDITION, COUNT_CHARACTERS_TO_USE, MAX_RESULTS_THRESHOLD from DUPLICATESCANNERPREFILTERCONFIG"
    + " join DUPLICATESCANNER on DUPLICATESCANNER.ID = DUPLICATESCANNERPREFILTERCONFIG.DUPLICATESCANNER_ID"
    + " where FILTER_NAME = '" + pFilterName + "'"
    + " and ENTITY_TO_SCAN_NAME = '" + pTargetEntity + "'";
    return db.table(query);
}

/*
 * Deletes the cached duplicate for the given id.
 * If there would only remain one item in the cluster after deletion, the whole cluster including the duplicate gets deleted.
 * In this case, all records marked as unrelated duplicate will be deleted aswell.
 *
 * @param {String} pDuplicateId Id of the duplicate to delete
 */
DuplicateScannerUtils.DeleteCachedDuplicate = function(pDuplicateId)
{
    let query = "select count(ID), CLUSTERID from DUPLICATECLUSTERS"
    + " where CLUSTERID in (select CLUSTERID from DUPLICATECLUSTERS where DUPLICATEID = '"+ pDuplicateId +"')"
    + " and DUPLICATEID != '"+ pDuplicateId +"'"
    + " group by CLUSTERID";

    let coundAndClusterId = db.array(db.ROW, query);
    let countDuplicatesInClusterWithoutParameterId = coundAndClusterId[0];
    let clusterId = coundAndClusterId[1];

    //If only one duplicate would be remaining,
    //the whole cluster has to be deleted because there are no more duplicates.
    //Otherwise delete just the single duplicate. The parameterized duplicate has been excluded via sql
    //therefore check for smaller/equals 1
    if(countDuplicatesInClusterWithoutParameterId <= 1)
    {
        let deleteStatements = [];
        deleteStatements.push(["DUPLICATECLUSTERS", "DUPLICATECLUSTERS.CLUSTERID = '"+ clusterId +"'"]);
        deleteStatements.push(["UNRELATEDDUPLICATES", "UNRELATEDDUPLICATES.CLUSTERID = '"+ clusterId +"'"]);

        db.deletes(deleteStatements);
    }
    else
    {
        db.deleteData("DUPLICATECLUSTERS", "DUPLICATECLUSTERS.DUPLICATEID = '"+ pDuplicateId +"'");
        //Delete all records where this duplicateId is mentioned
        DuplicateScannerUtils.DeleteAllUnrelatedDuplicateRelations(pDuplicateId);
    }
}

/*
 * Deletes all Clusters for the given target Entity.
 * No records markes as unrelated duplicate are being deleted.
 *
 * @param {String} pTargetEntity Entity which has been configured
 * @return Count of deleted rows
 */
DuplicateScannerUtils.DeleteDuplicateClustersByTargetEntity = function(pTargetEntity)
{
    return db.deleteData("DUPLICATECLUSTERS", "DUPLICATECLUSTERS.TARGET_ENTITY = '"+ pTargetEntity +"'")
}

/*
 * Updates the duplicate relations markes as unrelated for the given target entity.
 * All ClusterIds get updated with the new values if the same combination of duplicateIds
 * still exists in the DUPLICATECLUSTERS table.
 *
 * Afterwards, all records which contain a nonexistend clusterId are being deleted
 *
 * @param {String} pTargetEntity Name of Entity whose duplicates should be updated
 */
DuplicateScannerUtils.RefreshUnrelatedDuplicateRelations = function(pTargetEntity)
{
    /*
     * Update all records with the current valid clusterId where the same duplicateId combination exists
     */
    let INDEX_NEW_CLUSTERID = 0;
    let INDEX_OLD_CLUSTERID = 1;

    let query = "select dc1.CLUSTERID, ud.CLUSTERID from UNRELATEDDUPLICATES ud"
    + " join DUPLICATECLUSTERS dc1 on dc1.DUPLICATEID = ud.SOURCEDUPLICATEID"
    + " join DUPLICATECLUSTERS dc2 on dc2.DUPLICATEID = ud.UNRELATEDDUPLICATEID"
    + " where dc1.TARGET_ENTITY = '" + pTargetEntity + "'";

    let newIdOldIdRay = db.table(query);
    let updateStatements = [];

    //Build update statements to set new clusterIds
    for (let i = 0; i < newIdOldIdRay.length; i++)
    {
        let newClusterId = newIdOldIdRay[i][INDEX_NEW_CLUSTERID];
        let oldClusterId = newIdOldIdRay[i][INDEX_OLD_CLUSTERID];
        let updateColumns = ["CLUSTERID"];
        let condition = "UNRELATEDDUPLICATES.CLUSTERID = '" + oldClusterId + "'";

        let updateStatement = ["UNRELATEDDUPLICATES", updateColumns, null, [newClusterId], condition];

        updateStatements.push(updateStatement);
    }
    db.updates(updateStatements);

    /*
     * All unrelated duplicate ids that still exist in a cluster, have been updated with the new cluster id.
     * All records with a nonexistend clusterid can now be deleted because they haven't been detected as a duplicate any more.
     */
    let deleteCondition = "CLUSTERID not in (select dc1.CLUSTERID from DUPLICATECLUSTERS dc1)";
    db.deleteData("UNRELATEDDUPLICATES", deleteCondition);
}

/*
 * Creates a relation between two duplicates which means they are unrelated.
 * They will not appear in each others duplicate tab any more.
 * To remove this relation use DuplicateScannerUtils.DeleteUnrelatedDuplicateRelation
 *
 * @param {String} pSourceContactId Id of first duplicate
 * @param {String} pUnrelatedContactId Id of second duplicate
 * @param {String} pClusterId Id of the cluster in which the duplicates are aggregated
 * @returns {String} Number of Records inserted
 */
DuplicateScannerUtils.CreateUnrelatedDuplicateRelation = function(pSourceContactId, pUnrelatedContactId, pClusterId)
{
    let newUid = util.getNewUUID();
    let columns = ["ID", "SOURCEDUPLICATEID", "UNRELATEDDUPLICATEID", "CLUSTERID"];
    let values = [newUid, pSourceContactId, pUnrelatedContactId, pClusterId];

    return db.insertData("UNRELATEDDUPLICATES", columns, null, values);
}

/*
 * Gets the cluster id in which the given duplicate id exists
 *
 *  @param {String} pDuplicateId whose cluster id should be searched
 *  @returns {String} Cluster id
 */
DuplicateScannerUtils.GetClusterId = function(pDuplicateId)
{
    let duplicateIdsOfClusterWithoutUnrelated = SqlCondition.begin()
    .and("DUPLICATECLUSTERS.DUPLICATEID = '" + pDuplicateId + "'")
    .buildSql("select CLUSTERID from DUPLICATECLUSTERS");

    return db.cell(duplicateIdsOfClusterWithoutUnrelated);
}

/*
 * Deletes the "unrelated" relation between two duplicates
 *
 *  @param {String} pSourceDuplicateId Id of the source duplicate
 *  @param {String} pUnrelatedDuplicateId Id of the source duplicate
 *  @returns {String} Number of records deleted
 */
DuplicateScannerUtils.DeleteUnrelatedDuplicateRelation = function(pSourceDuplicateId, pUnrelatedDuplicateId)
{
    var cond = new SqlCondition();
    cond.andPrepare("UNRELATEDDUPLICATES.SOURCEDUPLICATEID", pSourceDuplicateId)
    cond.andPrepare("UNRELATEDDUPLICATES.UNRELATEDDUPLICATEID", pUnrelatedDuplicateId)
    let condition = db.translateCondition(cond.build());

    return db.deleteData("UNRELATEDDUPLICATES", condition);
}

/*
 * Deletes all relations to a duplicate id wether the id is the source or the unrelated duplicate in the relation
 *
 *  @param {String} pDuplicateId Duplicate id whose "unrelated" relations are to delete
 *  @returns {String} Number of records deleted
 */
DuplicateScannerUtils.DeleteAllUnrelatedDuplicateRelations = function(pDuplicateId)
{
    var cond = new SqlCondition();
    cond.orPrepare("UNRELATEDDUPLICATES.SOURCEDUPLICATEID", pDuplicateId)
    cond.orPrepare("UNRELATEDDUPLICATES.UNRELATEDDUPLICATEID", pDuplicateId)
    let condition = db.translateCondition(cond.build());

    return db.deleteData("UNRELATEDDUPLICATES", condition);
}

/*
 * Loads all other duplicates from the cluster in which the parameterized duplicate is located
 *
 *  @param {String} pDuplicateId
 *  @returns {String[]} Array of duplicate ids
 */
DuplicateScannerUtils.GetCachedDuplicatesForDuplicateId = function(pDuplicateId)
{
    let querySelectDuplicateContactIds = "select DUPLICATEID from DUPLICATECLUSTERS"
    + " where CLUSTERID in (select CLUSTERID from DUPLICATECLUSTERS"
    + " where DUPLICATEID = '"+ pDuplicateId +"')"
    + " and DUPLICATEID != '"+ pDuplicateId +"'"
    + " and DUPLICATEID not in (select UNRELATEDDUPLICATEID from UNRELATEDDUPLICATES where SOURCEDUPLICATEID = '"+ pDuplicateId +"')"
    + " and DUPLICATEID not in (select SOURCEDUPLICATEID from UNRELATEDDUPLICATES where UNRELATEDDUPLICATEID = '"+ pDuplicateId +"')";

    return db.array(db.COLUMN, querySelectDuplicateContactIds);
}

/*
 * Returns all duplicate ids which haven't been marked as unrelated for the given cluster id.
 *
 * @param {String} pClusterId The clusters id
 * @return {String[]} Array of duplicate ids excluding those marked as unrelated
 */
DuplicateScannerUtils.GetCachedDuplicatesForClusterId = function(pClusterId)
{
    let clusterIdRay = [];
    clusterIdRay.push(pClusterId);

    let duplicateIdsOfClusterWithoutUnrelated = SqlCondition.begin()
    .and("DUPLICATEID not in (select UNRELATEDDUPLICATES.UNRELATEDDUPLICATEID from UNRELATEDDUPLICATES)")
    .and("DUPLICATECLUSTERS.CLUSTERID = '" + clusterIdRay + "'")
    .buildSql("select DUPLICATEID from DUPLICATECLUSTERS");

    return db.array(db.COLUMN, duplicateIdsOfClusterWithoutUnrelated);
}

/*
 * Recreates the cached duplicate clusters based on the configured filters. <br />
 * The old clusters have to be deleted manually beforehand using "DeleteDuplicateClustersByTargetEntity".<br />
 * If there have already been ignored relations between duplicate records, it's advised to call "RefreshUnrelatedDuplicateRelations" after the recreation of the duplicates cache.<br />
 * Please check the documentation of the params on how to get the infos required.<br />
 * <br />
 * If the usage of an external webservice has been activated, the results will be narrowed down by the prefilter and<br />
 * will then be given to the pFormatValuesConsumeWebserviceCallback via parameter.<br />
 * To access the values it is advised to run thru the parameter like an array and access its value by key which is the index field name. The entity<br />
 * field names can be converted using DuplicateScannerUtils.TranslateEntityToIndexFields. For further infos see the example section below.<br />
 * <br />
 * Attention!<br />
 * If it is configured to use the external webservice callback the values have to be in the same format as they are in the parameter of the callback.<br />
 * <br />
 * @param {String} pFilterName Name of the filter to use
 * @param {String} pTargetEntity The target entity which has been assigned to the filters configuration
 * @param {String} pRecordsBlockSize The values which are checked get loaded in blocks.
 * @param {String} pFormatValuesConsumeWebserviceCallback Null if no external service is used otherwise a function with one parameter.
 * @return {Int} Count of duplicate clusters created
 *
 * @example
 * var filterName = "PersonDuplicates";
 * var targetEntity = "Person_entity";
 * var recordBlockSize = 5;
 *
 * let resultFields = DuplicateScannerUtils.LoadResultFields(filterName, targetEntity);
 *
 * let formatToJsonAndCallWsCallback = function(pPossibleDuplicatesRay)
 * {
 *      let indexResultFields = DuplicateScannerUtils.TranslateEntityToIndexFields(targetEntity, resultFields)
 *
 *      //Run thru every duplicate result an read out the resultfields
 *      for (let i = 0; i < pPossibleDuplicatesRay.length; i++)
 *      {
 *          for (let b = 0; b < resultFields.length; b++)
 *          {
 *              let entityFieldName = resultFields[b];
 *              let indexFieldName = indexResultFields[entityFieldName];
 *              //logging.log("Entity Field -> "+ pPossibleDuplicatesRay[i][indexFieldName]);
 *              //format values
 *          }
 *      }
 *      //call webservice
 *      //reformat results to same structure as before
 *      return pPossibleDuplicatesRay;
 * };
 *
 * DuplicateScannerUtils.DeleteDuplicateClustersByTargetEntity(targetEntity);
 *
 * DuplicateScannerUtils.RebuildDuplicatesCache(filterName, targetEntity, pRecordsBlockSize, formatToJsonAndCallWsCallback);
 *
 * DuplicateScannerUtils.RefreshUnrelatedDuplicateRelations(targetEntity);
 */
DuplicateScannerUtils.RebuildDuplicatesCache = function(pFilterName, pTargetEntity,
    pRecordsBlockSize, pFormatValuesConsumeWebserviceCallback)
{
    let useExternalWebservice = _DuplicateScannerUtils._isUseExternalWebservice(pFilterName, pTargetEntity);
    let alreadyIdentifiedIds = [];

    let duplicateFieldsConfig = DuplicateScannerUtils.LoadIndexFieldsConfiguration(filterName, targetEntity);
    let resultFields = DuplicateScannerUtils.LoadResultFields(filterName, targetEntity);

    //load all entity fields used in the prefilter
    let filterFieldConfigs = _DuplicateScannerUtils._getFieldConfigsFromFilterFields(pFilterName, pTargetEntity);
    //logging.log("filterFieldConfigs -> " + filterFieldConfigs);

    let entityFieldsToLoad = _DuplicateScannerUtils._getEntityFieldNamesFromFieldsConfig(duplicateFieldsConfig);

    //logging.log("entityFieldsToLoad initial-> " + entityFieldsToLoad);

    //If the fields from the prefilter aren't in the entity fields to load, add them manually
    if(filterFieldConfigs.length > 0)
    {
        let INDEX_FILTER_FIELD_NAME = 0;
        for (let i = 0; i < filterFieldConfigs.length; i++)
        {
            let filterFieldConfig = filterFieldConfigs[i];
            let filterFieldName = filterFieldConfig[INDEX_FILTER_FIELD_NAME];
            //logging.log("filterFieldConfig -> " +filterFieldConfig );
            if(entityFieldsToLoad.indexOf(filterFieldName) < 0)
            {
                //logging.log("Noch nicht vorhanden, hinzufügen -> ");
                duplicateFieldsConfig.push(filterFieldConfig);
                entityFieldsToLoad.push(filterFieldName);
            }
        }
    }
    
    //logging.log("entityFieldsToLoad vollsätndig-> " + entityFieldsToLoad);
    //logging.log("duplicateFieldsConfig vollsätndig-> " + duplicateFieldsConfig);
    
    let targetRecords = DuplicateScannerUtils.LoadEntityRecords(pTargetEntity, entityFieldsToLoad, 0, pRecordsBlockSize);
    //logging.log("Initialer Block geladen targetRecords-> " + JSON.stringify(targetRecords));

    let currentRecordIndex = pRecordsBlockSize;
    while(targetRecords.length > 0)
    {
        foundDuplicateIds = DuplicateScannerUtils.ScanRecords(pFilterName, pTargetEntity, targetRecords,
            duplicateFieldsConfig, resultFields, useExternalWebservice,
            pFormatValuesConsumeWebserviceCallback, alreadyIdentifiedIds);
        //logging.log("gefundene ids zum ignorieren foundDuplicateIds -> " + foundDuplicateIds);
        alreadyIdentifiedIds = alreadyIdentifiedIds.concat(foundDuplicateIds);
        //logging.log("Gesamte ignorierListe -> " + alreadyIdentifiedIds);
        if(targetRecords.length < pRecordsBlockSize)
        {
            logging.log("weniger records geladen als möglich => Ende der verfügbaren Records -> Abbrechen");
            break;
        }
        //logging.log("Nächster Block wird geladen mit startRow -> " + currentRecordIndex);

        targetRecords = DuplicateScannerUtils.LoadEntityRecords(pTargetEntity, entityFieldsToLoad,
            currentRecordIndex, pRecordsBlockSize);

        currentRecordIndex += pRecordsBlockSize;
    //logging.log("Nächster Block geladen mit targetRecords -> " + JSON.stringify(targetRecords));
    }
}

DuplicateScannerUtils.ScanRecords = function(pFilterName, pTargetEntity, pTargetRecordsData,
    pDuplicateFieldsConfig, pResultFields, pUseExternalWebservice, pFormatValuesConsumeWebserviceCallback, pAlreadyIdentifiedIds)
{
    //logging.log("in ScanRecords -> ");
    let foundDuplicateIds = [];

    //If the contact id loading query has no results, stop.
    //No ids should be deleted if an error has been made in this query.
    if(pTargetRecordsData.length <= 0)
        return;

    //First it gets checked if the current id has already been identified. If that's the case it'll continue with the next.
    //Otherwise an object gets build in the form of ["FilterFieldName" = "FilterFieldValueFromQuery"] with which a scan for possible duplicates get's started
    //logging.log("configured pResultFields -> " + pResultFields);
    var duplicatesToInsertQueries = [];
    for (b = 0; b < pTargetRecordsData.length; b++)
    {
        //logging.log("Nächster Datensatz in RebuildDuplicatesCache -> " + b);
        //        logging.log("Neuer Record -> " + pTargetRecordsData[b]);

        //logging.log("pTargetRecordsData[b] -> " + JSON.stringify(pTargetRecordsData[b]));
        let entityFieldValuesRay = DuplicateScannerUtils.BuildEntityFieldNameValueRays(pDuplicateFieldsConfig, pTargetRecordsData[b]);
        //logging.log("Werte des Datensatzes entityFieldValuesRay -> " + JSON.stringify(entityFieldValuesRay));

        //The first field in this Array must always be the configured id field. This is ensured using onValidation-logic
        let idField = entityFieldValuesRay[0][0];
        let idValue = entityFieldValuesRay[0][1];
        //        logging.log("idField -> " + idField);
        //        logging.log("idValue -> " + idValue);
        //logging.log("pTargetRecordsData[b][idField] -> " + pTargetRecordsData[b][idField]);
        //If the current Id has already been identified, continue
        if(pAlreadyIdentifiedIds.indexOf(pTargetRecordsData[b][idField]) > -1)
        {
            //logging.log("Id schon behandelt, continue; -> ");
            continue;
        }

        let foundDuplicates = _DuplicateScannerUtils._scanForDuplicates(pFilterName, pTargetEntity,
            entityFieldValuesRay, pResultFields, idField, idValue, pFormatValuesConsumeWebserviceCallback, pUseExternalWebservice)

        if(foundDuplicates == null || foundDuplicates.length == 0)
        {
            //logging.log("Keine Datensätze gefunden  continue;-> ");
            continue;
        }
        //logging.log("foundDuplicates.length nach _scanForDuplicates -> " + foundDuplicates.length);

        //Insert all found duplicate ids into an cache array because those ids don't have to be checked again lateron.

        for (let i = 0; i < foundDuplicates.length; i++)
        {
            let localId = foundDuplicates[i][indexsearch.FIELD_ID];
            //logging.log("foundDuplicates[i] -> " + foundDuplicates[i]);
            foundDuplicateIds.push(localId);
        }

        pAlreadyIdentifiedIds = pAlreadyIdentifiedIds.concat(foundDuplicateIds);

        //The duplicates list contains only the found duplicates to the original id, therefore it get's added manually
        foundDuplicateIds.push(pTargetRecordsData[b][idField]);
        //logging.log("foundDuplicates -> " + JSON.stringify(foundDuplicates));
        //logging.log("foundDuplicateIds -> " + JSON.stringify(foundDuplicateIds));

        let insertQueriesRay = _DuplicateScannerUtils._createInsertDuplicatesClusterQuery(foundDuplicateIds, pTargetEntity)
        duplicatesToInsertQueries = duplicatesToInsertQueries.concat(insertQueriesRay);
        foundDuplicateIds = [];
    }
    //logging.log("duplicatesToInsertQueries -> " + JSON.stringify(duplicatesToInsertQueries));
    db.inserts(duplicatesToInsertQueries, db.getCurrentAlias(), 10 * datetime.ONE_MINUTE);
    return foundDuplicateIds;
}

/* 
 * Searches for a cluster which contains the duplicates specified by the parameterized array. <br />
 * The contents of the cluster have to be identical, if no fitting cluster could be found an empty string is returned.
 * 
 * @param {String} pNewRecordId The id of the record which was used to scan for duplicates
 * @param {String[]} pDuplicateIds Duplicate ids used to search for a cluster containing them
 * @param {String} pTargetEntity Entity which has been configured
 * @returns {String} A clusterid if a matching cluster has been found, otherwise ""
 */
DuplicateScannerUtils.CacheNewScanResults = function(pNewRecordId, pDuplicateIds, pTargetEntity)
{
    let duplicateIds = [];
    //Run thru every duplicate result and read out the id. 
    //Do it now to have a simple array on all usages lateron.
    for (let i = 0; i < pDuplicateIds.length; i++)
    {
        let duplicateContactId = pDuplicateIds[i][indexsearch.FIELD_ID];
        duplicateIds.push(duplicateContactId);
    }
    
    let clusterId = DuplicateScannerUtils.GetClusterWithIdenticalDuplicates(duplicateIds);
    
    //If no cluster has beend found, create a new one with all found duplicateIds, 
    //otherwise add the id to the existing cluster
    let idRayToInsert = [];
    if(clusterId == undefined || clusterId == null || clusterId == "")
    {
        idRayToInsert = duplicateIds;
        idRayToInsert.push(pNewRecordId);
    }
    else
        idRayToInsert.push(pNewRecordId);
    
    //logging.log("idRayToInsert -> " + idRayToInsert);
    insertQueriesRay = _DuplicateScannerUtils._createInsertDuplicatesClusterQuery(idRayToInsert, pTargetEntity, clusterId)

    return db.inserts(insertQueriesRay);
}

/* 
 * Searches for a cluster which contains the duplicates specified by the parameterized array. <br />
 * The contents of the cluster have to be identical, if no fitting cluster could be found an empty string is returned.
 * 
 * @param {String[]} pDuplicateIds Duplicate ids which should be in the same cluster
 * @returns {String} Id of the cluster which contains all given duplicate ids or ""
 */
DuplicateScannerUtils.GetClusterWithIdenticalDuplicates = function(pDuplicateIds)
{
    let RESULT_NO_CLUSTER_FOUND = "";

    if(pDuplicateIds.length < 1)
        return RESULT_NO_CLUSTER_FOUND;
    
    let clusterIdConditionBuilder = SqlCondition.begin();
    
    for (let i = 0; i < pDuplicateIds.length; i++) 
    {
        clusterIdConditionBuilder.andSqlCondition("CLUSTERID in (select CLUSTERID from DUPLICATECLUSTERS where DUPLICATEID = '" + pDuplicateIds[i] + "')", "1=2")
    }

    clusterIdCondition = clusterIdConditionBuilder.buildSql("select distinct CLUSTERID from DUPLICATECLUSTERS", "1=2");
    
    let foundClusterId = db.cell(clusterIdCondition);
    //logging.log("clusterid des clusters der die gleichen ids enthält-> " + foundClusterId);
    if(foundClusterId == null || foundClusterId == "")
        return RESULT_NO_CLUSTER_FOUND;
    
    let duplicatesInClusterCondition = SqlCondition.begin()
    .andPrepare("DUPLICATECLUSTERS.CLUSTERID", foundClusterId)
    .buildSql("select DUPLICATEID from DUPLICATECLUSTERS");
    let duplicatesInCluster = db.array(db.COLUMN, duplicatesInClusterCondition);
    //logging.log("länge gefundener cluster -> " + duplicatesInCluster.length);
    //logging.log("länge der angefragten ids -> " + pDuplicateIds.length);
    /* 
     * A cluster has been searched which contains all duplicate ids as specified via parameter.
     * There's the possibility that this cluster contains even more duplicates than specified via the parameter.
     * In this case, the cluster and the parameterized duplicateids are not identical 
     * which means a new cluster has to be created.
     */
    if(pDuplicateIds.length != duplicatesInCluster.length)
        return RESULT_NO_CLUSTER_FOUND;
    else
        return foundClusterId;
}

DuplicateScannerUtils.LoadEntityRecords = function(pTargetEntity, pEntityFields, pStartRow, pCountRecordsToLoad)
{
    let getRowsConfig = entities.createConfigForLoadingRows()
    .entity(pTargetEntity)
    .fields(pEntityFields)
    .count(pCountRecordsToLoad)
    .startrow(pStartRow);
    return entities.getRows(getRowsConfig);
}
/*
 * Loads the configured resultfields as array
 *
 *  @param {String} pFilterName Name of the filter
 *  @param {String} pTargetEntity Entity which has been configured
 *  @returns {String[]} Resultfields as array
 */
DuplicateScannerUtils.LoadResultFields = function(pFilterName, pTargetEntity)
{
    let duplicateResultFields = SqlCondition.begin()
    .andPrepare("DUPLICATESCANNER.FILTER_NAME", pFilterName)
    .andPrepare("DUPLICATESCANNER.ENTITY_TO_SCAN_NAME", pTargetEntity)
    .buildSql("select dsrfc.ENTITY_FIELD_NAME from DUPLICATESCANNERRESULTFIELDCONFIG dsrfc join DUPLICATESCANNER on DUPLICATESCANNER.ID = dsrfc.DUPLICATESCANNER_ID"
        , "1=2");

    return db.array(db.COLUMN, duplicateResultFields);
}

/*
 * Loads the fields and their configuration.
 * One field record is in the following format:
 * [ENTITY_FELD, IS_ID, USE_FOR_SEARCH]
 * Example:
 * ["FIRSTNAME", false, true]
 *
 *  @param {String} pFilterName Name of the filter
 *  @param {String} pTargetEntity Entity which has been configured
 *  @returns {String[[]]} An Array of Arrays in the format described above
 */
DuplicateScannerUtils.LoadIndexFieldsConfiguration = function(pFilterName, pTargetEntity)
{
    let duplicateIndexFieldConfigurations = SqlCondition.begin()
    .andPrepare("DUPLICATESCANNER.FILTER_NAME", pFilterName)
    .andPrepare("DUPLICATESCANNER.ENTITY_TO_SCAN_NAME", pTargetEntity)
    .buildSql("select dsic.ENTITY_FIELD_NAME, dsic.IS_ID_FIELD, dsic.USE_FOR_INDEX_DUPLICATE_SEARCH from DUPLICATESCANNERINDEXCONFIG dsic join DUPLICATESCANNER on DUPLICATESCANNER.ID = dsic.DUPLICATESCANNER_ID"
        , "1=2", "order by dsic.IS_ID_FIELD desc");

    return db.table(duplicateIndexFieldConfigurations);
}

/*
 * Scans for duplicates based on the configured prefilters and the configured indexfields.<br />
 * All configured fields values have to be present in "pValuesToCheck"<br />
 * First the prefilters get applied one after another until the count of the returned data is in the allowed threshold.<br />
 * Then, the duplicate search using the index starts. All fields which have been configured will be used here.<br />
 *<br />
 * If the usage of an external webservice has been activated, the result will then be given to the pFormatValuesConsumeWebserviceCallback via parameter.<br />
 * To access the values it is advised to run thru the parameter like an array and access its value by key which is the index field name. The entity
 * field names can be converted using DuplicateScannerUtils.TranslateEntityToIndexFields
 * <br />
 * <br />
 * Attention!<br />
 * If it's a single ScanForDuplicates call it doesn't matter what the callback returns because after the callback, no more modifications follow before
 * returning the data.<br />
 * If it's inside the RebuildCache the values have to be in the same format as the parameter
 *
 * @param {String} pFilterName Name of the filter
 * @param {String} pTargetEntity Respective target entity
 * @param {{"key", "value"}} pValuesToCheck An object with key value pairs which hold the name of the entity field as key and it's value as value. See the example "valuesToCheck"
 * @param {function} pFormatValuesConsumeWebserviceCallback Null if no external service is used otherwise a function with one parameter.
 * If the function is called is based on the configuration of the current scanner
 * @returns {[["key", "value"]]} Array of Key-Value-Pairs based on the configured resultfields, if an external webservices was used
 * the structure is defined by the parameterized function "pFormatValuesConsumeWebserviceCallback"
 *
 * @example
 * var filterName = "PersonDuplicates";
 * var targetEntity = "Person_entity";
 *
 * //Values to check, the same fields as configured
 * let valuesToCheck = {};
 * valuesToCheck["CONTACTID"] = "c7ddf982-0e58-4152-b82b-8f5673b0b729";
 * valuesToCheck["FIRSTNAME"] = "Tim";
 * valuesToCheck["GENDER"] = "m                                   ";
 *
 * let formatToJsonAndCallWsCallback = function(pPossibleDuplicatesRay)
 * {
 *     let indexResultFields = DuplicateScannerUtils.TranslateEntityToIndexFields(targetEntity, resultFields)
 *
 *     //Run thru every duplicate result an read out the resultfields
 *     for (let i = 0; i < pPossibleDuplicatesRay.length; i++)
 *     {
 *         for (let b = 0; b < resultFields.length; b++)
 *         {
 *             let entityFieldName = resultFields[b];
 *             let indexFieldName = indexResultFields[entityFieldName];
 *             //logging.log("Entity Field -> "+ pPossibleDuplicatesRay[i][indexFieldName]);
 *             //format values
 *         }
 *     }
 *     //call webservice
 *     //reformat results to same structure as before
 *     return pPossibleDuplicatesRay;
 * };
 *
 * //The result values can be accessed as seen above in "formatToJsonAndCallWsCallback"
 * DuplicateScannerUtils.ScanForDuplicates(filterName, targetEntity, valuesToCheck,
 * formatToJsonAndCallWsCallback);
 */
DuplicateScannerUtils.ScanForDuplicates = function(pFilterName, pTargetEntity, pValuesToCheck,
    pFormatValuesConsumeWebserviceCallback)
{
    let useExternalWebservice = _DuplicateScannerUtils._isUseExternalWebservice(pFilterName, pTargetEntity);
    let resultFields = DuplicateScannerUtils.LoadResultFields(pFilterName, pTargetEntity);
    let duplicateFieldsConfig = DuplicateScannerUtils.LoadIndexFieldsConfiguration(pFilterName, pTargetEntity);
    //logging.log("duplicateFieldsConfig -> " + duplicateFieldsConfig);
    let entityFieldsToLoad = _DuplicateScannerUtils._getEntityFieldNamesFromFieldsConfig(duplicateFieldsConfig);
    //logging.log("entityFieldsToLoad " + entityFieldsToLoad)
    //If the fields from the prefilter aren't in the entity fields to load, add them manually
    let filterFieldConfigs = _DuplicateScannerUtils._getFieldConfigsFromFilterFields(pFilterName, pTargetEntity);
    //logging.log("filterFieldConfigs " + filterFieldConfigs)

    if(filterFieldConfigs.length > 0)
    {
        let INDEX_FILTER_FIELD_NAME = 0;
        for (let i = 0; i < filterFieldConfigs.length; i++)
        {
            let filterFieldConfig = filterFieldConfigs[i];
            let filterFieldName = filterFieldConfig[INDEX_FILTER_FIELD_NAME];
            //logging.log("filterFieldName" + filterFieldName)
            //logging.log("filterFieldConfig -> " +filterFieldConfig );
            if(entityFieldsToLoad.indexOf(filterFieldName) < 0)
            {
                //logging.log("Noch nicht vorhanden, hinzufügen -> ");
                duplicateFieldsConfig.push(filterFieldConfig);
            }
        }
    }

    let entityFieldValuesRay = DuplicateScannerUtils.BuildEntityFieldNameValueRays(duplicateFieldsConfig, pValuesToCheck);
    //The first field in this Array must always be the configured id field.
    //logging.log("ray " + entityFieldValuesRay.toSource())
    let idField = entityFieldValuesRay[0][0];
    let idValue = entityFieldValuesRay[0][1];

    return _DuplicateScannerUtils._scanForDuplicates(pFilterName, pTargetEntity,
        entityFieldValuesRay, resultFields, idField, idValue,
        pFormatValuesConsumeWebserviceCallback, useExternalWebservice)
}

/*
 * Executes a indexsearch.lookupIndexField for eacht entity field in the parameterized array
 * and returns it as Map.
 *
 *  @param {String} pEntityName ...Name of the entity
 *  @param {String[]} pEntityFields Array of the entities Fields to translate to index fields
 *  @returns Map-like object where (key = entity field) and (value = index field)
 *
 *  @example
 *  let entityResultFields = ["LASTNAME"];
 *  let entityIndexFields = DuplicateScannerUtils.TranslateEntityToIndexFields("Person_entity", entityResultFields);
 *
 *  logging.log(entityIndexFields["LASTNAME"]);//=> "LASTNAME_value"
 */
DuplicateScannerUtils.TranslateEntityToIndexFields = function(pEntityName, pEntityFields)
{
    let entityIndexFields = {};
    for (let i = 0; i < pEntityFields.length; i++)
    {
        let entityFieldName = pEntityFields[i];

        //todo remove if api has been fixed
        if(entityFieldName.startsWith(pEntityName))
            entityFieldName = entityFieldName.replace(pEntityName + ".", "");

        let indexField = indexsearch.lookupIndexField(pEntityName, entityFieldName);
        entityIndexFields[pEntityFields[i]] = indexField;
    }
    return entityIndexFields;
}

/*
 *
 * Merges the source person into the target person. 
 * This 
 * - replaces the source's with the target's contactid in a predefined set of tables.
 * - resets the standard communications of the source contact and keeps the ones of the target.
 * - updates participants of campaigns and removes obsolet ones(which would be duplicates)
 * - deletes the source person and contact
 * - deletes the duplicate record, if one exists
 * - deletes all unrelated-duplicate-relations containing the source contact id
 *
 *  @param {String} pSourceContactId The contact to be integrated into another
 *  @param {String} pTargetContactId The contact in which the source gets integrated
 *  @returns {String}
 */
DuplicateScannerUtils.MergePerson = function(pSourceContactId, pTargetContactId)
{
    let updateStatementsCurrentAlias = [];
    let updateStatementsSystemAlias = [];
    let deleteStatements = [];

    var sourcePersonId = db.cell("select PERSON_ID from CONTACT where CONTACTID = '" + pSourceContactId + "'");
    var tableInfosCurrentAlias = _DuplicateScannerUtils._getMergeUpdateTableInfosCurrentAlias();
    var tableInfosSystemAlias = _DuplicateScannerUtils._getMergeUpdateTableInfosSystemAlias();

    updateStatementsCurrentAlias = updateStatementsCurrentAlias.concat(_DuplicateScannerUtils._buildUpdateResetStandardCommunications(pSourceContactId));
    updateStatementsCurrentAlias = updateStatementsCurrentAlias.concat(_DuplicateScannerUtils._buildUpdateContactIdStatements(tableInfosCurrentAlias, pSourceContactId, pTargetContactId));
    updateStatementsCurrentAlias = updateStatementsCurrentAlias.concat(_DuplicateScannerUtils._buildUpdateAttachParticipantsToNewContactQuery("CAMPAIGNPARTICIPANT", "CONTACT_ID", "CAMPAIGN_ID", pSourceContactId, pTargetContactId));

    updateStatementsSystemAlias = updateStatementsSystemAlias.concat(_DuplicateScannerUtils._buildUpdateContactIdStatements(tableInfosSystemAlias, pSourceContactId, pTargetContactId));

    deleteStatements = deleteStatements.concat(_DuplicateScannerUtils._buildDeleteRemoveObsoleteParticipantsRecordsQuery("CAMPAIGNPARTICIPANT", "CONTACT_ID", "CAMPAIGN_ID", pSourceContactId, pTargetContactId));
    deleteStatements = deleteStatements.concat(_DuplicateScannerUtils._buildDeletePersonAndContactQuery(sourcePersonId, pSourceContactId));
    deleteStatements = deleteStatements.concat(_DuplicateScannerUtils._buildDeleteCachedUnrelatedDuplicateQuery(pSourceContactId));

    //logging.log("updateStatementsCurrentAlias -> " + JSON.stringify(updateStatementsCurrentAlias));
    //logging.log("deleteStatements -> " + JSON.stringify(deleteStatements));

    let affectedRowsCurrentAlias = db.updates(updateStatementsCurrentAlias);
    let affectedRowsSystemAlias = db.updates(updateStatementsSystemAlias, SqlUtils.getSystemAlias());
    let deletedRows = db.deletes(deleteStatements)

    //logging.log("affectedRowsCurrentAlias -> " + affectedRowsCurrentAlias);
    //logging.log("deletedRows -> " + deletedRows);

    DuplicateScannerUtils.DeleteCachedDuplicate(pSourceContactId);

    return (affectedRowsCurrentAlias > 0 && deletedRows >= 2);
}

DuplicateScannerUtils.CreateMergeSuccessActivity = function(pSourceContactId, pTargetContactId, pCurrentContactId, pContext)
{
    var activityDataForInsert = {
        subject: translate.withArguments("A %0 record has been merged", [pContext]),
        content: translate.withArguments("%0 with ID \"%1\" has been integrated into the %0 with the ID \"%2\"", [pContext, pSourceContactId, pTargetContactId]),
        //categoryKeywordId: $KeywordRegistry.ac
        directionKeywordId: $KeywordRegistry.activityDirection$internal(),
        responsibleContactId: pCurrentContactId
    };
    var activityLinks = [[pContext, pTargetContactId]];

    return ActivityUtils.insertNewActivity(activityDataForInsert, activityLinks, null, db.getCurrentAlias());
}

DuplicateScannerUtils.MergeOrganisation = function(pSourceContactId, pTargetContactId)
{
    let updateStatements = [];
    let updateStatementsSystemAlias = [];
    let deleteStatements = [];

    let querySourceOrganisationId = SqlCondition.begin()
    .and("CONTACTID = '" + pSourceContactId + "'")
    .buildSql("select ORGANISATION_ID from CONTACT");
    var sourceOrganisationId = db.cell(querySourceOrganisationId);

    var tableInfosCurrentAlias = _DuplicateScannerUtils._getMergeUpdateTableInfosCurrentAlias();
    var tableInfosSystemAlias = _DuplicateScannerUtils._getMergeUpdateTableInfosSystemAlias();

	updateStatementsCurrentAlias = updateStatementsCurrentAlias.concat(_DuplicateScannerUtils._buildUpdateResetStandardCommunications(pSourceContactId));
    updateStatementsCurrentAlias = updateStatementsCurrentAlias.concat(_DuplicateScannerUtils._buildUpdateContactIdStatements(tableInfosCurrentAlias, pSourceContactId, pTargetContactId));
    updateStatementsCurrentAlias = updateStatementsCurrentAlias.concat(_DuplicateScannerUtils._buildUpdateAttachParticipantsToNewContactQuery("CAMPAIGNPARTICIPANT", "CONTACT_ID", "CAMPAIGN_ID", pSourceContactId, pTargetContactId));

    updateStatementsSystemAlias = updateStatementsSystemAlias.concat(_DuplicateScannerUtils._buildUpdateContactIdStatements(tableInfosSystemAlias, pSourceContactId, pTargetContactId));

    deleteStatements = deleteStatements.concat(_DuplicateScannerUtils._buildDeleteRemoveObsoleteParticipantsRecordsQuery("CAMPAIGNPARTICIPANT", "CONTACT_ID", "CAMPAIGN_ID", pSourceContactId, pTargetContactId));
    deleteStatements = deleteStatements.concat(_DuplicateScannerUtils._buildDeleteOrganisationAndContactQuery(sourceOrganisationId, pSourceContactId));
    deleteStatements = deleteStatements.concat(_DuplicateScannerUtils._buildDeleteCachedUnrelatedDuplicateQuery(pSourceContactId));

    //logging.log("updateStatementsCurrentAlias -> " + JSON.stringify(updateStatementsCurrentAlias));
    //logging.log("deleteStatements -> " + JSON.stringify(deleteStatements));

    let affectedRowsCurrentAlias = db.updates(updateStatementsCurrentAlias);
    let affectedRowsSystemAlias = db.updates(updateStatementsSystemAlias, SqlUtils.getSystemAlias());
    let deletedRows = db.deletes(deleteStatements)

    //logging.log("affectedRowsCurrentAlias -> " + affectedRowsCurrentAlias);
    //logging.log("deletedRows -> " + deletedRows);

    DuplicateScannerUtils.DeleteCachedDuplicate(pSourceContactId);

    return (affectedRowsCurrentAlias > 0 && deletedRows >= 2);
}

/*
 * Creates an array of arrays containing the entity field name paired with it's value.
 *
 * @param {[]} pDuplicateFieldsConfig An Array with the configured fields in the form of [ENTITY_FIELD, IS_ID, USE_FOR_SEARCH]. @see LoadDuplicateIndexFieldsConfiguration()
 * @param {{"key", "value"}} pTargetRecordData One record containing the values for the configured fields. It's in the format of {"key(=EntityFieldName", "Value"}
 * @return {[[]]} An array of arrays containing the entity field name an its value. [["CONTACTID", "d786045c-8b21-4f22-b6d9-72be9f61c04d"]]
 * @example
 * pDuplicateFieldsConfig
 * ["CONTACTID", true, false]
 * ["FIRSTNAME", false, true]
 *
 * pTargetRecordData
 * ["d786045c-8b21-4f22-b6d9-72be9f61c04d", "PETER"]
 *
 * => [["CONTACTID", "d786045c-8b21-4f22-b6d9-72be9f61c04d"], ["FIRSTNAME", "PETER"]]
 */
DuplicateScannerUtils.BuildEntityFieldNameValueRays = function(pDuplicateFieldsConfig, pTargetRecordData)
{
    let INDEX_CONFIG_ENTITY_FIELD = 0;
    let INDEX_CONFIG_USE_FOR_SEARCH = 2;
    let entityFieldValuesRay = [];
    /*
    * Based on the parameterized filter field names and the values loaded via the query,
    * an array which contains records in the style of ["FilterFieldName", "FilterFieldValueFromQuery"] gets created.
    * This is mandatory to run the scan for this record.
    */
    for (a = 0; a < pDuplicateFieldsConfig.length; a++)
    {
        //        logging.log("pDuplicateFieldsConfig[a][1] -> " + pDuplicateFieldsConfig[a][INDEX_CONFIG_ENTITY_FIELD]);
        //        logging.log(" pTargetRecordData[a] -> " + pTargetRecordData[a]);
        let entityField = pDuplicateFieldsConfig[a][INDEX_CONFIG_ENTITY_FIELD];
        entityFieldValuesRay.push([entityField, pTargetRecordData[entityField], pDuplicateFieldsConfig[a][INDEX_CONFIG_USE_FOR_SEARCH]])
    }
    return entityFieldValuesRay.length > 0 ? entityFieldValuesRay : [["", ""]];
}

function _DuplicateScannerUtils() {}

var INDEX_FILTER_CONDITION = 0;
var INDEX_COUNT_CHARS_TO_USE = 1;
var INDEX_MAX_RESULTS_THRESHOLD = 2;

var INDEX_TABLE_NAME = 0;
var INDEX_COLUMN_NAME = 1;
var INDEX_CONDITION = 2;

_DuplicateScannerUtils._buildUpdateResetStandardCommunications = function(pSourceContactId)
{
    return [["COMMUNICATION", ["ISSTANDARD"], null, ["0"], "CONTACT_ID = '" + pSourceContactId + "'"]];
}

_DuplicateScannerUtils._getEntityFieldNamesFromFieldsConfig = function(pDuplicateFieldsConfig)
{
    let entityFieldsToLoad = [];
    for (field in pDuplicateFieldsConfig)
    {
        entityFieldsToLoad.push(pDuplicateFieldsConfig[field][0]);
    }
    return entityFieldsToLoad;
}
_DuplicateScannerUtils._getFieldConfigsFromFilterFields = function(pFilterName, pTargetEntity)
{
    let resultFields = [];
    let filterFields = [];
    let filters = DuplicateScannerUtils.loadFilters(pFilterName, pTargetEntity)

    for (let i = 0; i < filters.length; i++)
    {
        let filter = JSON.parse(filters[i][0]).filter;
        let fields = JditoFilterUtils.getFilterFields(filter.childs);
        filterFields = filterFields.concat(fields);
    }
    for (let i = 0; i < filterFields.length; i++)
    {
        let filterField = filterFields[i];
        resultFields.push([filterField, 0, 0]);
    }
    return resultFields;
}

/*
 * @see DuplicateScannerUtils.ScanForDuplicates for the documentation
 */
_DuplicateScannerUtils._scanForDuplicates = function(pFilterName, pTargetEntity, pFilterFieldValueRays,
pResultFields, pRecordIdFieldToIgnore, pRecordIdValueToIgnore, pFormatValuesConsumeWebserviceCallback, pUseExternalWebservice)
{
    let possibleDuplicates = [];
    let ignoredRecordFilter = _DuplicateScannerUtils._getIgnoreRecordFilter(pRecordIdFieldToIgnore, pRecordIdValueToIgnore, pTargetEntity);
    let configuredFilters = _DuplicateScannerUtils._loadFilters(pFilterName, pTargetEntity);


    //logging.log("Found filters -> " + configuredFilters);

    let preFilter = null;
    
    //Only run the prefilter if filters have been configured. If not, run the indexsearch based on the field configuration
    if(configuredFilters != null && configuredFilters.length > 0)
    {
        //To ensure the record which the current search is based on isnt found as result, the other configured filters get appended to
        //the filter of said records to ignore
        configuredFilters = [ignoredRecordFilter].concat(configuredFilters);

        preFilter =_DuplicateScannerUtils._applyPreFilter(pTargetEntity, configuredFilters, pFilterFieldValueRays);

        //logging.log("preFilter welcher Elemente im erlaubten bereich ausgibt -> " + preFilter);
        
        //The scan can be executed even without any prefilters. If a prefilter has been configured but doesn't match the 
        //threshold criteria no search shall be run.
        if(preFilter == null)
            return null;
    }
    
    //No prefilter and no filterfields => No indexsearch
    if(preFilter == null && pFilterFieldValueRays.length < 1)
        return null;
    
    //If at this point the prefilter is null but a search has to be executed, add the ignorefilter manually that the search doesn't find the base record as duplicate to itself.
    //This is the case if no prefilter but indexfields are configured.
    if(preFilter == null)
        preFilter = ignoredRecordFilter;
    
    possibleDuplicates = _DuplicateScannerUtils._callIndexSearch(pTargetEntity, preFilter, pFilterFieldValueRays, pResultFields, 100);
    //logging.log("possibleDuplicates -> " + JSON.stringify(possibleDuplicates));
    
    if(possibleDuplicates == null)
        return null;

    possibleDuplicates = possibleDuplicates[indexsearch.HITS];

    if(pUseExternalWebservice && possibleDuplicates.length > 0 && pFormatValuesConsumeWebserviceCallback != null)
        possibleDuplicates = pFormatValuesConsumeWebserviceCallback.apply(this, [possibleDuplicates]);

//    //logging.log("pTargetEntity -> " + pTargetEntity);
//    //logging.log("preFilter -> " + preFilter);
//    //logging.log("pFilterFieldValueRays -> " + pFilterFieldValueRays);
//    //logging.log("pRecordIdFieldToIgnore -> " + pRecordIdFieldToIgnore);
//    //logging.log("possibleDuplicates -> " + possibleDuplicates);

    return possibleDuplicates;
}

/*
 * Returns a bool which say wether or not an external service should be used
 *
 * @param {String} pFilterName Name of the filter
 * @param {String} pTargetEntity Entity which has been configured
 *  @returns {Bool} True = use, False = no use
 */
_DuplicateScannerUtils._isUseExternalWebservice = function(pFilterName, pTargetEntity)
{
    let scannerUseExternalWebserviceQuery = SqlCondition.begin()
    .andPrepare("DUPLICATESCANNER.FILTER_NAME", pFilterName)
    .andPrepare("DUPLICATESCANNER.ENTITY_TO_SCAN_NAME", pTargetEntity)
    .buildSql("select EXTERNAL_SERVICE_USAGE_ALLOWED from DUPLICATESCANNER"
        , "1=2");

    let isUseWebservice = db.cell(scannerUseExternalWebserviceQuery);
    return (isUseWebservice == 0) ? false : true;
}

/*
 * Applies... the configured prefilters. The pre filter is used to narrow the records to be searched by the duplicate scan service
 * The filters get applied sequentially one after another.
 * If the number of results equal or lower as the configured threshold and greater than zero, the current filter combination gets returned.
 * If no more filters are available and the number of results still exceed the threshold, null gets returned.
 * If the number of results reach zero  while aplying filters, null gets returned
 * The reason is, that if a huge count of records were to be used, the time to search for duplicates would be substantially longer.
 * If the prefilters to not have the desired effect it is advised to modify the configured filter conditions
 *
 *  @param {String} pTargetEntity Entity which has been configured
 *  @param {String[[]]} pFilterCountCharactersToUseRay Array of Arrays containing the configuration of the filters.
 * The structure is as follows: [INDEX_FILTER_CONDITION, INDEX_COUNT_CHARS_TO_USE, INDEX_MAX_RESULTS_THRESHOLD]
 *  @param {String[[]]} pFilterFieldValueRays Array of Arrays containing the name of a used field and its value.
 * Use "DuplicateScannerUtils.BuildEntityFieldNameValueRays". The fieldnames and values have to be in the same order
 *  @returns {String} Null if the records count wasnt inside the threshold, the used combined filter to achieve a successfull prefiltering
 */
_DuplicateScannerUtils._applyPreFilter = function(pTargetEntity, pFilterCountCharactersToUseRay, pFilterFieldValueRays)
{
//    logging.log("#pFilterFieldValueRays#" + JSON.stringify(pFilterFieldValueRays))
    var combinedFilter = {};

    for (i = 0; i < pFilterCountCharactersToUseRay.length; i++) 
    {
        var filter = pFilterCountCharactersToUseRay[i][INDEX_FILTER_CONDITION];
//        logging.log("#1#filter>>" + filter)
        let countCharsOfValueToUse = pFilterCountCharactersToUseRay[i][INDEX_COUNT_CHARS_TO_USE];
        let maxResultsThreshold = pFilterCountCharactersToUseRay[i][INDEX_MAX_RESULTS_THRESHOLD];
        
        if(filter == null || filter == "")
            continue;

        filter = JSON.parse(filter);

        let filterValuesObject = {};
        for (a = 0; a < pFilterFieldValueRays.length; a++)
        {
            filterValuesObject[pFilterFieldValueRays[a][0]] = pFilterFieldValueRays[a][1];
        }

        //logging.log("filterValuesObject zum füllen des jsons -> " + JSON.stringify(filterValuesObject));
        /*
         * Insert the values into the current filter. Has to be here so that only the new filter
         * and therefore the combinedFilter incrementally gets filled and not always everything multiple times.
         */
        //logging.log("1 ###### filter zum befüllen mit werten-> " + JSON.stringify(filter));
        filter.filter.childs = _DuplicateScannerUtils._insertValuesInFilterTemplate(filter.filter.childs, filterValuesObject, countCharsOfValueToUse);
        //logging.log("2 ###### gefüllter filter prefilter index-> " + JSON.stringify(filter));

        if(i == 0)
            combinedFilter = filter.filter;
        else
        {
            //logging.log("3 ###### aktueller combinedFilter -> " + JSON.stringify(combinedFilter));
            //logging.log("4 ###### gefüllter filter -> " + JSON.stringify(filter));
            //Extend the current combined filter with the next filter condition to further refine the results
            //It seems to always contain one child element at the root
            //combinedFilter.childs.push(filter.filter.childs);
            let newCombinedFilterChilds = combinedFilter.childs.concat(filter.filter.childs);
            combinedFilter.childs = newCombinedFilterChilds;
        }

        //logging.log("5 ###### combinedFilter + gefüllter filter vor indexsuche-> " + JSON.stringify(combinedFilter));

        filter.filter = combinedFilter;
        //logging.log("6 ###### completeFilter -> " + JSON.stringify(filter));

        //Workaround to load the smallest possible resultset because only the TOTALHITS are relevant at this time
        //Only load "indexsearch.FIELD_ID" and a resultSet size of 1
        let searchResult = _DuplicateScannerUtils._callIndexSearch(pTargetEntity, JSON.stringify(filter), [],
            [], 1);//todo use again after this has been fixed!! insert the local id after fix
        //logging.log("searchResults hits length -> " + searchResult[indexsearch.HITS].length);

        //        if(searchResult[indexsearch.TOTALHITS] < 80)//todo entfernen?!
        //        {
        //            for (let i = 0; i < searchResult[indexsearch.HITS].length; i++)
        //            {
        //                logging.log("Treffer Nr -> " + i);
        //                //searchResults hits 0 -> {#ADITO_SEARCH_ID=1868bd3a-05af-4b7f-a633-e3aec50ac45c, _index_group_=Person, #ADITO_SEARCH_TYPE=Person, firstname_value=Peter, _local_id_=1868bd3a-05af-4b7f-a633-e3aec50ac45c}
        //                let localId = searchResult[indexsearch.HITS][i]["_local_id_"];
        //                let firstname = searchResult[indexsearch.HITS][i]["firstname_value"];
        //                let indexGroup = searchResult[indexsearch.HITS][i]["_index_group_"];
        //                logging.log("localId -> " + localId);
        //                logging.log("firstname -> " + firstname);
        //                logging.log("indexGroup -> " + indexGroup);
        //            }
        //        }

        let totalHits = searchResult[indexsearch.TOTALHITS]

        //logging.log("totalHits -> " + totalHits);

        if(totalHits > maxResultsThreshold)
        {
            //logging.log("zu viele rows gefundenn nächsten Filter anwenden -> totalHits:" + totalHits + " maxResultsThreshold:" + maxResultsThreshold);
            //Found more rows than allowed by the threshold, run next filter to narrow the results
            continue;
        }
        else if(totalHits <= 0)
        {
            return null;
        }
        else
        {
            //we're in the treshold, return the valid filter. The filter gets used lateron.
            return JSON.stringify(filter);
        }
    }
    //logging.log("zu viele rows und keine filter mehr -> ");
    return null;
}

/*
 * Runs the indexsearch based on the given parameters.
 * If the "pEntityFieldValueRays" is empty, only the prefilters get applied as pattern.
 * if not, the prefilters will be applies as pattern and the contents of "pEntityFieldValueRays" get applies as filter.
 *
 * @param {String} pTargetEntity Entity which has been configured
 * @param {String} pPreFilterJson The prefilters
 * @param {String[[]]} pEntityFieldValueRays Array of Arrays containing the name of a used field and its value.
 * Use "DuplicateScannerUtils.BuildEntityFieldNameValueRays". The fieldnames and values have to be in the same order. NotNull!->Empty Array
 * @param {String} pResultFields The result field config. Use "DuplicateScannerUtils.LoadResultFields"
 * @param {String} pResultSetRows todo
 * @returns {[["key", "value"]]} Array of Key-Value-Pairs based on the configured pResultFields
 */
_DuplicateScannerUtils._callIndexSearch = function(pTargetEntity, pPreFilterJson, pEntityFieldValueRays, pResultFields, pResultSetRows)
{
    let indexPattern = null;
    let filterPattern = null;
    
    //The pPreFilterJson is never null because it always contains at least the default ignore record filter
    indexPattern = indexsearch.buildQueryFromSearchCondition(pPreFilterJson);

    let filterPatternConfig = _DuplicateScannerUtils._buildFilterPatternConfig(pEntityFieldValueRays, pTargetEntity);

    if(filterPatternConfig != null)
        filterPattern = indexsearch.buildPatternString(filterPatternConfig);
    
    //The indexPattern can't be null because it is required to run the search.
    if(indexPattern == null)
        return null;
    
    let indexQuery = indexsearch.createIndexQuery()
                                .setPattern(indexPattern)
                                .setEntities([pTargetEntity])
                                //.addSearchFields("Person_entity.FIRSTNAME", "Person_entity.LASTNAME", "Person_entity.CONTACTID")
                                //.setRows(pResultSetRows);
    indexQuery = _DuplicateScannerUtils._setResultFields(indexQuery, pResultFields);

    if(filterPattern != null)
        indexQuery = indexQuery.addFilter(filterPattern);
    
    logging.log("indexQuery.getPattern -> " + indexQuery.getPattern());
    logging.log("indexQuery.getFilters -> " + indexQuery.getFilters());

    return indexsearch.searchIndex(indexQuery);
}

/*
 * Sets each field of the given array as resultfield on the given query.
 * Supports indexsearch internal fields aswell
 * (indexsearch.FIELD_ID, indexsearch.FIELD_TITLE, indexsearch.FIELD_TYPE, indexsearch.FIELD_DESCRIPTION)
 *
 * @param {IndexQuery} pIndexQuery An indexquery created with "indexsearch.createIndexQuery()"
 * @param {String[]} pResultFields The result field config. Use "DuplicateScannerUtils.LoadResultFields"
 * @returns {IndexQuery} IndexQuery with the resultfields set
 */
_DuplicateScannerUtils._setResultFields = function(pIndexQuery, pResultFields)
{
    let resultIndexFields = [];
    let resultFields = [];
    for (let i = 0; i < pResultFields.length; i++)
    {
        if(pResultFields[i] == indexsearch.FIELD_ID
            || pResultFields[i] == indexsearch.FIELD_TITLE
            || pResultFields[i] == indexsearch.FIELD_TYPE
            || pResultFields[i] == indexsearch.FIELD_DESCRIPTION)
            {
            resultIndexFields.push(pResultFields[i]);
        }
        else
            resultFields.push(pResultFields[i]);
    }

    //    if(resultIndexFields.length == 0 && resultFields.length == 0)
    //        pIndexQuery = pIndexQuery.addResultIndexFields([indexsearch.FIELD_ID]);
    //    else
    //    {
    if(resultIndexFields.length > 0)
        pIndexQuery = pIndexQuery.addResultIndexFields(resultIndexFields);

    if(resultFields.length > 0)
    {
        pIndexQuery = pIndexQuery.addResultFields(resultFields);
    }
    //    }

    //logging.log("resultFields -> " + resultFields);
    return pIndexQuery;
}

/*
 * Translates the fields and the respective value which are configured to use for duplicates to a pattern config to use with
 * an index search query.
 *
 * @param {String[[]]} pEntityFieldValueRays Array of Arrays containing the name of a used field and its value.
 * @param {String} pTargetEntity Entity which has been configured
 * @returns {PatternConfig} PatternConfig created with "indexsearch.createPatternConfig()", null if the creation wasn't successful
 */
_DuplicateScannerUtils._buildFilterPatternConfig = function(pEntityFieldValueRays, pTargetEntity)
{
    //The index to get the fields value for USE_FOR_SEARCH
    //Structure of this array is [ENTITY_FIELD, FIELD_VALUE, USE_FOR_SEARCH]
    let INDEX_CONFIG_USE_FOR_SEARCH = 2;
    let filterPatternConfig = null;

    //logging.log("pEntityFieldValueRays.length -> " + pEntityFieldValueRays.length);
    if(pEntityFieldValueRays.length > 0)
    {
        filterPatternConfig = indexsearch.createPatternConfig();
        for (let i = 0; i < pEntityFieldValueRays.length; i++)
        {
            let entityFieldValue = pEntityFieldValueRays[i][1];
            let entityFieldName = pEntityFieldValueRays[i][0];

            if(pEntityFieldValueRays[i][INDEX_CONFIG_USE_FOR_SEARCH] == 0 || entityFieldValue == "")
                continue;

            //logging.log("entityFieldValue -> " + entityFieldValue);
            //logging.log("entityFieldName -> " + entityFieldName);
            let indexField = indexsearch.lookupIndexField(pTargetEntity, entityFieldName);

            //logging.log("indexField -> " + indexField);
            var filterTerm = indexsearch.createTerm(entityFieldValue)
            .setIndexField(indexField)
            .setFuzzySearchFactor(0);

            filterPatternConfig.and(filterTerm);
        }
    }
    return (filterPatternConfig == null || filterPatternConfig.isEmpty()) ? null : filterPatternConfig;
}

/*
 * Creates the queries to insert new duplicates into a new cluster based on the pDuplicatesRay
 *
 * @param {String[]} pDuplicatesRay Array of duplicate ids
 * @param {String} pTargetEntity Entity which has been configured
 * @returns {String[]} Array of insert queries
 */
_DuplicateScannerUtils._createInsertDuplicatesClusterQuery = function (pDuplicatesRay, pTargetEntity)
{
    return _DuplicateScannerUtils._createInsertDuplicatesClusterQuery(pDuplicatesRay, pTargetEntity, util.getNewUUID());
}

/*
 * Creates the queries to insert new duplicates into a new cluster based on the pDuplicatesRay
 *
 * @param {String[]} pDuplicatesRay Array of duplicate ids
 * @param {String} pTargetEntity Entity which has been configured
 * @param {String} pClusterId Clusters id to which the duplicates are in relation
 * @returns {String[]} Array of insert queries
 */
_DuplicateScannerUtils._createInsertDuplicatesClusterQuery = function (pDuplicatesRay, pTargetEntity, pClusterId)
{
    let duplicatesToInsertQueries = [];
    let cols = ["ID", "CLUSTERID", "DUPLICATEID", "TARGET_ENTITY"];

    if(pClusterId == undefined || pClusterId == null || pClusterId == "")
        pClusterId = util.getNewUUID();
            
    for (i = 0; i < pDuplicatesRay.length; i++)
    {
        let newId = util.getNewUUID();
        let valuesToInsert = [newId, pClusterId, pDuplicatesRay[i], pTargetEntity];

        duplicatesToInsertQueries.push(["DUPLICATECLUSTERS", cols, null, valuesToInsert]);
    }
    return duplicatesToInsertQueries;
}

/*
 * Deletes all clusters
 *
 * @returns {String} Count of records deleted
 */
_DuplicateScannerUtils._deleteDuplicateClusters = function ()
{
    return db.deleteData("DUPLICATECLUSTERS");
}

/*
 * All records with contactId = sourceContactId get updated, which are not assigned to the same "group" as the targetContactId.
 * This is because otherwise there would now be in total two "participants" in the same "group" as opposed to one before.
 * Also if they already are in the same "group" those records shouldn't be updated because it would lead to the same outcome.
 *
 * Mandatory: All records ignored for the time being have to be deleted aswell! See #_DuplicateScannerUtils._buildRemoveObsoleteParticipantsRecordsDeleteQuery
 * @returns {String[]} Query to update records
 */
_DuplicateScannerUtils._buildUpdateAttachParticipantsToNewContactQuery = function (pTableName, pContactIdColumn, pAssignableIdColumn, pSourceContactId, pTargetContactId, updateStatements)
{
    var selectAssignableIdsOfTargetContactQuery = "select " + pAssignableIdColumn
                                                    + " from " + pTableName
                                                    + " where " + pContactIdColumn + " = '" + pTargetContactId + "'";

    let updateCondition = pAssignableIdColumn
    + " not in"
    + " (" + selectAssignableIdsOfTargetContactQuery + ")"
    + " and " + pContactIdColumn + " = '" + pSourceContactId + "'";

    return [[pTableName, [pContactIdColumn], null, [pTargetContactId], updateCondition]];
}


_DuplicateScannerUtils._buildDeleteRemoveObsoleteParticipantsRecordsQuery = function (pTableName, pContactIdColumn, pAssignableIdColumn, pSourceContactId, pTargetContactId, updateStatements)
{
    
//DELETE FROM CAMPAIGNPARTICIPANT 
// WHERE ( CAMPAIGN_ID in (select ab.CAMPAIGN_ID from (select CAMPAIGN_ID, CONTACT_ID from CAMPAIGNPARTICIPANT) ab where ab.CONTACT_ID = '64a51ec3-e75d-4415-8aa2-a00a1e9be0b0') and CAMPAIGN_ID = '51960918-3b24-4bac-8f1c-3892bf210f6d') 

    var selectAssignableIdsOfTargetContactQuery = "select " + pAssignableIdColumn
                                                    + " from " + pTableName
                                                    + " where " + pContactIdColumn + " = '" + pTargetContactId + "'";

    let deleteCondition = pAssignableIdColumn + " in"
    + " (" + selectAssignableIdsOfTargetContactQuery + ")"
    + " and " + pAssignableIdColumn + " = '" + pSourceContactId + "'";
    let recordsToDelete = [];
    recordsToDelete.push([pTableName, deleteCondition]);
    return recordsToDelete;
}


/*
 * Creates Queries to delete from the PERSON and CONTACT table.
 *
 * @returns {String[]} Queries to delete
 */
_DuplicateScannerUtils._buildDeletePersonAndContactQuery = function(pSourcePersonId, pSourceContactId)
{
    let recordsToDelete = []
    recordsToDelete.push(["PERSON", "PERSONID = '" + pSourcePersonId + "'"]);
    recordsToDelete.push(["CONTACT", "CONTACTID = '" + pSourceContactId + "'"]);
    return recordsToDelete;
}

_DuplicateScannerUtils._buildDeleteOrganisationAndContactQuery = function(pSourceOrganisationId, pSourceContactId)
{
    let recordsToDelete = []
    recordsToDelete.push(["ORGANISATION", "ORGANISATIONID = '" + pSourceOrganisationId + "'"]);
    recordsToDelete.push(["CONTACT", "CONTACTID = '" + pSourceContactId + "'"]);
    return recordsToDelete;
}

_DuplicateScannerUtils._buildDeleteCachedUnrelatedDuplicateQuery = function(pSourceContactId)
{
    let recordsToDelete = []
    recordsToDelete.push(["UNRELATEDDUPLICATES", "SOURCEDUPLICATEID = '" + pSourceContactId + "'"]);
    recordsToDelete.push(["UNRELATEDDUPLICATES", "UNRELATEDDUPLICATEID = '" + pSourceContactId + "'"]);
    return recordsToDelete;
}

/*
 * Creates a filter JSON which excludes the field and it's value using the operator "NOT_EQUAL"
 *
 * @param {String} pRecordIdFieldToIgnore Field to be ignored
 * @param {String} pRecordIdValueToIgnore The fields value
 * @param {String} pTargetEntity Entity which has been configured
 * @returns {String[]} Array where 0 = filter, 1 = null(INDEX_COUNT_CHARS_TO_USE), 2 = null(INDEX_MAX_RESULTS_THRESHOLD)
 */
_DuplicateScannerUtils._getIgnoreRecordFilter = function(pRecordIdFieldToIgnore, pRecordIdValueToIgnore, pTargetEntity)
{
    let ignoreFilterJson = JSON.stringify({"entity":pTargetEntity,"filter":{"type":"group","operator":"AND","childs":[{"type":"row","name":pRecordIdFieldToIgnore,"operator":"NOT_EQUAL","value":pRecordIdValueToIgnore,"key":"","contenttype":"TEXT"}]}});

    return [ignoreFilterJson, null, null];
}

_DuplicateScannerUtils._buildUpdateContactIdStatements = function(pTableInfos, pSourceContactId, pTargetContactId)
{
    let statements = [];

    for (let i = 0; i < pTableInfos.length; i++)
    {
        let tableInfo = pTableInfos[i];
        let updateStatement = _DuplicateScannerUtils._buildStatement(tableInfo, pSourceContactId, pTargetContactId);
        statements.push(updateStatement);
    }
    return statements;
}

_DuplicateScannerUtils._buildStatement = function(pTableinfos, pSourceContactId, pTargetContactId)
{
    let tableName = pTableinfos[INDEX_TABLE_NAME];
    let columnName = pTableinfos[INDEX_COLUMN_NAME];
    let additionalCondition = pTableinfos[INDEX_CONDITION];

    let condition = columnName + " = '" + pSourceContactId + "'";

    if(additionalCondition != "")
        condition += " and ( " + additionalCondition + ") ";

    return [tableName, [columnName], null, [pTargetContactId], condition];
}

/*
 * Contains all Tables and their fields which may contain the contact id to be replaced for the data alias
 *
 * @returns {String[[]]} Array in the format [TableName, ContactIdColumnName, AdditionalCondition]
 */
_DuplicateScannerUtils._getMergeUpdateTableInfosCurrentAlias = function()
{
    var tableInfos = new Array();
    tableInfos.push(["AB_APPOINTMENTLINK", "OBJECT_ROWID", ""]);
    tableInfos.push(["AB_CTILOG", "CONTACT_ID", ""]);
    tableInfos.push(["AB_OBJECTRELATION", "AB_OBJECTRELATIONID", ""]);
    tableInfos.push(["AB_OBJECTRELATION", "OBJECT1_ROWID", ""]);
    tableInfos.push(["AB_OBJECTRELATION", "OBJECT2_ROWID", ""]);
    tableInfos.push(["AB_LOGHISTORY", "TABLENAMEID", ""]);
    tableInfos.push(["ADDRESS", "CONTACT_ID", ""]);
    tableInfos.push(["BULKMAILRECIPIENT", "CONTACT_ID", ""]);
    tableInfos.push(["BULKMAIL", "TESTING_CONTACT_ID", ""]);
    tableInfos.push(["CAMPAIGN", "EMPLOYEE_CONTACT_ID", ""]);
    tableInfos.push(["CAMPAIGNSTEP", "EMPLOYEE_CONTACT_ID", ""]);
    tableInfos.push(["COMMRESTRICTION", "CONTACT_ID", ""]);
    tableInfos.push(["COMMRESTRICTION", "EMPLOYEE_INVOLVED", ""]);
    tableInfos.push(["COMMUNICATION", "CONTACT_ID", ""]);
    tableInfos.push(["COMPETITION", "CONTACT_ID", ""]);
    tableInfos.push(["CONTRACT", "CONTACT_ID", ""]);
    tableInfos.push(["LETTERRECIPIENT", "CONTACT_ID", ""]);
    tableInfos.push(["OBJECTMEMBER", "CONTACT_ID", ""]);
    tableInfos.push(["OFFER", "CONTACT_ID", ""]);
    tableInfos.push(["PRODUCT", "CONTACT_ID", ""]);
    tableInfos.push(["PRODUCTPRICE", "CONTACT_ID", ""]);
    tableInfos.push(["SALESORDER", "CONTACT_ID", ""]);
    tableInfos.push(["SALESPROJECT", "CONTACT_ID", ""]);
    tableInfos.push(["TASK", "REQUESTOR_CONTACT_ID", ""]);
    tableInfos.push(["TASK", "EDITOR_CONTACT_ID", ""]);
    tableInfos.push(["TASKLINK", "OBJECT_ROWID", ""]);
    tableInfos.push(["ACTIVITY", "RESPONSIBLE", ""]);
    tableInfos.push(["DSGVO", "CONTACT_ID", ""]);
    tableInfos.push(["DSGVOINFO", "CONTACT_ID", ""]);
    tableInfos.push(["TIMETRACKING", "CONTACT_ID", ""]);
    tableInfos.push(["ACTIVITYLINK", "OBJECT_ROWID", ""]);
    tableInfos.push(["AB_ATTRIBUTERELATION", "OBJECT_ROWID", ""]);

    return tableInfos;
}

/*
 * Contains all Tables and their fields which may contain the contact id to be replaced for the system alias
 *
 * @returns {String[[]]} Array in the format [TableName, ContactIdColumnName, AdditionalCondition]
 */
_DuplicateScannerUtils._getMergeUpdateTableInfosSystemAlias = function()
{
    var tableInfos = new Array();
    tableInfos.push(["ASYS_CALENDARLINK", "DBID", ""]);
    tableInfos.push(["ASYS_BINARIES", "ROW_ID", "TABLENAME = 'CONTACT'"]);
    return tableInfos;
}

/*
 * Loads all filters for the requested scanner in the format of:
 * [FILTER_CONDITION, COUNT_CHARACTERS_TO_USE, MAX_RESULTS_THRESHOLD]
 *
 * @param {String} pFilterName Name of the filter
 * @param {String} pTargetEntity Entity which has been configured
 * @returns {String[[]]} Array of arrays in the format [FILTER_CONDITION, COUNT_CHARACTERS_TO_USE, MAX_RESULTS_THRESHOLD]
 */
_DuplicateScannerUtils._loadFilters = function(pFilterName, pTargetEntity)
{
    let query = "select FILTER_CONDITION, COUNT_CHARACTERS_TO_USE, MAX_RESULTS_THRESHOLD from DUPLICATESCANNERPREFILTERCONFIG"
    + " join DUPLICATESCANNER on DUPLICATESCANNER.ID = DUPLICATESCANNERPREFILTERCONFIG.DUPLICATESCANNER_ID"
    + " where FILTER_NAME = '" + pFilterName + "'"
    + " and ENTITY_TO_SCAN_NAME = '" + pTargetEntity + "'";
    return db.table(query);
}

/*
 * Starts at the pJsonRootNode and recursively traverses thru the jsons "row" and "group" nodes to fill the fields values based on
 * the values in pEntitiyFieldAndValueMap.
 *
 * @param {JSON} pJsonRootNode A filter Json
 * @param {Map{"key", "value"}}} pEntitiyFieldAndValueMap {"key", "value"}
 * @returns {JSON} The JSON with filled values based on pEntitiyFieldAndValueMap and pCountCharsOfValueToUse
 */
_DuplicateScannerUtils._insertValuesInFilterTemplate = function(pJsonRootNode, pEntitiyFieldAndValueMap, pCountCharsOfValueToUse)
{
    for(var filterChildNode in pJsonRootNode)
    {
        var currentNode = pJsonRootNode[filterChildNode];
        if(currentNode.type == "row")
        {
            let fieldName = currentNode.name;
            let fieldValue = pEntitiyFieldAndValueMap[fieldName];
            pCountCharsOfValueToUse = parseInt(pCountCharsOfValueToUse, 10);

            if(fieldValue == null)
            {
                //logging.log("Duplicate Scan: Requested value for field " + fieldName + " not present in the provided valueslist");
                continue;
            }

            if(_DuplicateScannerUtils._isNotNullAndANumber(pCountCharsOfValueToUse)
                && _DuplicateScannerUtils._isValueLongerThanCharsToUse(fieldValue.length, pCountCharsOfValueToUse))
                {
                fieldValue = fieldValue.substring(0, pCountCharsOfValueToUse);
            }

            pJsonRootNode[filterChildNode].value = fieldValue;
            pJsonRootNode[filterChildNode].key = fieldValue;
        }
        else
        {
            //Type of curren node is "group", now run thru it's nodes
            let populatedChildNodes = _DuplicateScannerUtils._insertValuesInFilterTemplate(currentNode.childs, pEntitiyFieldAndValueMap, pCountCharsOfValueToUse);
            pJsonRootNode[filterChildNode].childs = populatedChildNodes;
        }
    }
    return pJsonRootNode;
}

/*
 * Returns wether or not a value should be substring'd
 *
 * @return true if pCountCharsOfValueToUse is a number, greater than 0 and smaller than the values length
 */
_DuplicateScannerUtils._isValueLongerThanCharsToUse = function(pValueLength, pCountCharsOfValueToUse)
{
    return !isNaN(pCountCharsOfValueToUse)
    && pCountCharsOfValueToUse > 0
    && pValueLength > pCountCharsOfValueToUse;
}

/*
 * Returns wether or not the parameter isnt null and a number or not
 *
 * @param {String} pCountCharsOfValueToUse Hopefully a number
 * @returns {String} True if parameter isnt null and a number, False if it's null or no number
 */
_DuplicateScannerUtils._isNotNullAndANumber = function(pCountCharsOfValueToUse)
{
    return pCountCharsOfValueToUse != null && !isNaN(pCountCharsOfValueToUse);
}