Skip to content
Snippets Groups Projects
process.js 56.42 KiB
import("system.logging");
import("Employee_lib");
import("KeywordData_lib");
import("Context_lib");
import("system.util");
import("system.datetime");
import("system.translate");
import("system.neon");
import("system.vars");
import("system.db");
import("system.project");
import("system.entities");
import("Sql_lib");
import("Keyword_lib");

/**
 * Provides functions for the work with attributes, like
 * listing the available attributes for a context.
 * Don't instanciate this!
 * 
 * @class
 */
function AttributeUtil () {}

/**
 * @return all Contexts (Object types) which can be the usage of attributes
 */
AttributeUtil.getPossibleUsageContexts = function() 
{
    return [
        "Organisation",
        "Person",
        "Contract",
        "Product",
        "Activity",
        "Offer",
        "Order",
        "Employee",
        "Salesproject",
        "Campaign",
        "DocumentTemplate",
        "SupportTicket",
        "Leadimport",
        "ImportField"
    ];
}

/**
 * Gives an array of all available attributes for a context. This is used in the possibleItems
 * process for the attribute id in AttributeRelation
 * 
 * @param {String} pObjectType the object type (= context)
 * @param {boolean} [pIncludeGroups=false]
 * @param {String[]} [pFilteredAttributeIds=[]] Whitleist of attribute ids
 * @param {Object} [pAttributeCount=null] Object with attribute ids and their count
 * 
 * @return {String[]} array of attributeIds
 */
AttributeUtil.getPossibleAttributes = function (pObjectType, pIncludeGroups, pFilteredAttributeIds, pAttributeCount)
{
    if (pObjectType == null || (pFilteredAttributeIds && pFilteredAttributeIds.length == 0))
        return [];
    
    var attrSelect = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_PARENT_ID, ATTRIBUTE_TYPE")
                        .from("AB_ATTRIBUTE")
                        .join("AB_ATTRIBUTEUSAGE", "AB_ATTRIBUTEID = AB_ATTRIBUTE_ID")
                        .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)
                        .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, SqlBuilder.NOT_EQUAL())
                        .and("ATTRIBUTE_ACTIVE = 1");
    if (pAttributeCount)
    {
        for (let attributeId in pAttributeCount)
        {
            attrSelect.and(newWhere()
                            .or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId, SqlBuilder.NOT_EQUAL())
                            .or("AB_ATTRIBUTEUSAGE.MAX_COUNT", pAttributeCount[attributeId], SqlBuilder.GREATER())
                            .or("AB_ATTRIBUTEUSAGE.MAX_COUNT is null")
            );
        }
    }
        
    if (pFilteredAttributeIds)
    {
        var filteredIdChildren = AttributeUtil.getAllChildren(pFilteredAttributeIds);
        var allFilteredIds = pFilteredAttributeIds.concat(filteredIdChildren);
        
        attrSelect.andIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", allFilteredIds, SqlBuilder.IN())
    }

    if (!pIncludeGroups)
        attrSelect.and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.GROUP, SqlBuilder.NOT_EQUAL());
    
    var attributes = attrSelect.table();
    
    //filter out groups without usable children
    if (pIncludeGroups && pAttributeCount)
    {
        var parentIds = {};
        attributes.forEach(function (attribute)
        {
            this[attribute[1]] = true;
        }, parentIds);
        attributes = attributes.filter(function (attribute)
        {
            return attribute[2].trim() != $AttributeTypes.GROUP || this[attribute[0]];
        }, parentIds);
    }
    
    return attributes.map(function (attribute)
    {
        return attribute[0];
    });
}

/**
 * searches for possiblevalues for a atttribute and returns these. The values depend on the attributeType
 * 
 * @param {String} pAttributeId the id of the attribute
 * @param {Boolean} pAttributeType type of the attribute that is specified with pAttributeId;
 *                                 The type needs to be passed to the function for better performance 
 *                                 (loading the type via attribute several times would be too slow)
 * @param {Boolean} [pIncludeInactives=false] specifies if only active attributevalues or actives + inactives shall be returned, 
 *                                            this is important when you want to search for attributevalues 
 * 
 * @return {Array} 2D-array with [ID, value] als elements if the given attributeType has possible items. if not null is returned
 */
AttributeUtil.getPossibleListValues = function (pAttributeId, pAttributeType, pIncludeInactives)
{
    var attributeId = pAttributeId;
    var attrType = pAttributeType.trim();
    var onlyActives = (pIncludeInactives == undefined ? false : pIncludeInactives);
    if (attrType == $AttributeTypes.COMBO.toString())
    {
        var valuesSelect = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_NAME")
                                .from("AB_ATTRIBUTE")
                                .where("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", attributeId)
                                .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE);
                                
        if (onlyActives)
            valuesSelect.and("AB_ATTRIBUTE.ATTRIBUTE_ACTIVE", "1");
        
        var valueList = valuesSelect.orderBy("SORTING asc")
                                    .table();
            
        for (let i = 0; i < valueList.length; i++)
        {
            valueList[i][1] = translate.text(valueList[i][1]);
        }
        return valueList;
    }
    else if (attrType == $AttributeTypes.BOOLEAN.toString())
    {
        return [
            ["1", translate.text("Yes")],
            ["0", translate.text("No")]
            ];
    }
    else if (attrType == $AttributeTypes.KEYWORD.toString())
    {
        var attrKeyword = newSelect("DROPDOWNDEFINITION")
                                .from("AB_ATTRIBUTE")
                                .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", attributeId)
                                .cell();
            
        var keywords = KeywordData.getSimpleData(attrKeyword, null, onlyActives);
        return keywords;
    }
    else if (attrType == $AttributeTypes.OBJECTSELECTION)
    {
        var [module, filter] = newSelect("DROPDOWNDEFINITION, DROPDOWNFILTER")
                                    .from("AB_ATTRIBUTE")
                                    .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", attributeId)
                                    .arrayRow();
        var objects = [];
        if (module)
        {
            var uid = "#UID";
            if (module == "Employee_entity")
                uid = "SHORT_UID";
            
            var title = "#CONTENTTITLE";
            var config = entities.createConfigForLoadingRows()
                .entity(module)
                .fields([uid, title]);
            if (filter)
            {
                filter = JSON.parse(filter);
                if (filter.filter)
                    config.filter(JSON.stringify(filter.filter));
            }
            var rows = entities.getRows(config);
            for (let i = 0, l = rows.length; i < l; i++)
                objects.push([rows[i][uid], rows[i][title]])
        }
        return objects;
    }
    else
        return null;
}

/**
 * returns the name of an attribute with all parent attribute names
 * 
 * @param {String} pAttributeId the id of the attribute
 * @param {Boolean} [pSimpleName=false] Use only the name of the attribute and not the names of the parents.
 * @param {Boolean} [pTranslate=true] translate the name
 * @param {Number} [pStartLayer=0] Group names to omit. Example: The attribute is "Departments / Distribution / Field staff".
 *          If you set this value to 1, "Departments / " will be removed, if set to 2, "Departments / Distribution / " will be removed and so on.
 *          The last name will never be removed to avoid an empty result if something is wrong.
 * 
 * @return {String} the name of the attribute
 */
AttributeUtil.getFullAttributeName = function (pAttributeId, pSimpleName, pTranslate, pStartLayer) 
{
    if (pSimpleName === undefined)
        pSimpleName = false;
    if (pTranslate === undefined)
        pTranslate = true;
    
    if (!pAttributeId)
        return "";
    if (pSimpleName)
        return AttributeUtil.getSimpleAttributeName(pAttributeId, pTranslate);
    var attributeNames = [];
    var attribute;
    do {
        attribute = newSelect("ATTRIBUTE.ATTRIBUTE_NAME, PARENT1.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_PARENT_ID")
                        .from("AB_ATTRIBUTE", "ATTRIBUTE")
                        .leftJoin("AB_ATTRIBUTE", "ATTRIBUTE.ATTRIBUTE_PARENT_ID = PARENT1.AB_ATTRIBUTEID", "PARENT1")
                        .leftJoin("AB_ATTRIBUTE", "PARENT1.ATTRIBUTE_PARENT_ID = PARENT2.AB_ATTRIBUTEID", "PARENT2")
                        .where(["AB_ATTRIBUTE", "AB_ATTRIBUTEID", "ATTRIBUTE"], pAttributeId)
                        .arrayRow();

        if (attribute.length > 0)
        {
            attributeNames.unshift(attribute[0]);
            if (attribute[1])
                attributeNames.unshift(attribute[1]);
            if (attribute[2])
                attributeNames.unshift(attribute[2]);
            pAttributeId = attribute[3];
        }
        else
            pAttributeId = "";
    } while (pAttributeId);
    
    if (pStartLayer)
        attributeNames = attributeNames.slice(pStartLayer < attributeNames.length ? pStartLayer : -1);
        
    if (pTranslate)
    {
        attributeNames = attributeNames.map(function (name)
        {
            return translate.text(name);
        });
    }
    return attributeNames.join(" / ");
}

/**
 * returns the name of an attribute
 * 
 * @param {String} pAttributeId the id of the attribute
 * @param {boolean} [pTranslate] if the name should be translated
 * 
 * @return {String} the name of the attribute
 */
AttributeUtil.getSimpleAttributeName = function (pAttributeId, pTranslate) 
{
    var attributeName = newSelect("ATTRIBUTE_NAME")
                            .from("AB_ATTRIBUTE")
                            .whereIfSet("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId)
                            .cell(true, "");
    if (pTranslate)
        attributeName = translate.text(attributeName);
    return attributeName;
}

/**
 * returns the ids of all subordinated attributes of an attribute
 * 
 * @param {String|Array} pAttributeIds the id(s) of the attribute(s)
 * 
 * @result {String[]} array with the ids of every subordinated attribute
 */
AttributeUtil.getAllChildren = function (pAttributeIds)
{
    var childIds = [];
    if (typeof(pAttributeIds) == "string")
        pAttributeIds = [pAttributeIds];
        
    while (pAttributeIds.length > 0)
    {
        pAttributeIds = newSelect("AB_ATTRIBUTEID")
                            .from("AB_ATTRIBUTE")
                            .where("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeIds, SqlBuilder.IN())
                            .arrayColumn();

        if (pAttributeIds.length > 0)
            childIds = childIds.concat(pAttributeIds);
    }
    return childIds;
}

/**
 * checks if an attribute has attribute relations
 * 
 * @param {String} pAttributeId the id of the attribute
 * 
 * @result {boolean} true if it has relations
 */
AttributeUtil.hasRelations = function (pAttributeId)
{
    if (!pAttributeId)
        return false;
    return new AttributeRelationQuery()
        .attributeId(pAttributeId)
        .getAttributeCount() != 0;
}

/**
 * returns the type of an attribute
 * 
 * @param {String} pAttributeId the id of the attribute
 * 
 * @result {String} attribute type
 */
AttributeUtil.getAttributeType = function (pAttributeId)
{
    if (!pAttributeId)
        return "";

    return newSelect("ATTRIBUTE_TYPE")
                .from("AB_ATTRIBUTE")
                .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId)
                .cell()
                .trim();
}

AttributeUtil.hasAttributes = function (pObjectType)
{
    if (!pObjectType)
        return false;
    return newSelect("count(*)")
                .from("AB_ATTRIBUTEUSAGE")
                .join("AB_ATTRIBUTE", "AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)
                .and("AB_ATTRIBUTE.ATTRIBUTE_ACTIVE", "1")
                .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, SqlBuilder.NOT_EQUAL())
                .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.GROUP, SqlBuilder.NOT_EQUAL())
                .cell() != "0"; //TODO: is there a way exists could be used?
}

/*********************************************************************************************************************/

/**
 * Provides functions for the work with attributeRelations, getting the value of an attributeRelation for an object.
 * Don't instanciate this!
 * 
 * @class
 */
function AttributeRelationUtils () {}

/**
 * @deprecated use AttributeRelationQuery
 * 
 * gets the value of an attributeRelation for one dataset (e. g. a person)
 * 
 * @param {String} pAttributeId attribute-id
 * @param {String} pObjectRowId row-id of the dataset
 * @param {String} [pObjectType=null] object-type
 * @param {String} [pGetViewValue=false] if true the values are resolved and formatted
 * @param {String} [pGetAttrname=false] if true the attributename is also returned
 * 
 * @return {String|String[]|null} the value of the attribute or an array of attrname and value [attrname, value] (if pGetAttrname is true)
 */
AttributeRelationUtils.getAttribute = function (pAttributeId, pObjectRowId, pObjectType, pGetViewValue, pGetAttrname)
{
    var attributeQuery = new AttributeRelationQuery(pObjectRowId, pAttributeId, pObjectType);
    
    if (pGetViewValue)
        attributeQuery.includeDisplayValue();
    if (pGetAttrname)
        attributeQuery.includeFullAttributeName();
    
    var attribute = attributeQuery.getSingleAttribute();
    var value = pGetViewValue ? attribute.displayValue : attribute.value;
    
    return pGetAttrName ? [attribute.fullAttributeName, value] : value;
}

/**
 * Get a SqlBuilder already containing the full select for attributes.
 * @param {String[]} pFields array of all fields which should be selected
 * @param {String} pObjectRowId object rowid
 * @param {String} [pObjectType=null] object-type
 * 
 * @return {SqlBuilder} a already filled SqlBuilder
 */
AttributeRelationUtils.getAttributeSqlBuilder = function (pFields, pObjectRowId, pObjectType)
{
    return newSelect(pFields)
                .from("AB_ATTRIBUTERELATION")
                .join("AB_ATTRIBUTE", "AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                .leftJoin("AB_ATTRIBUTE COMBOVAL", $AttributeTypes.COMBO.databaseField + " = COMBOVAL.AB_ATTRIBUTEID")
                .whereIfSet("AB_ATTRIBUTERELATION.OBJECT_ROWID", pObjectRowId)
                .andIfSet("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType);
}

/**
 * @deprecated use AttributeRelationQuery
 * gets all attributes for a dataset
 * 
 * @param {String} pObjectRowId object rowid
 * @param {String} [pObjectType=null] object-type
 * @param {String} [pUseAttributeIds=0] if 0 the full attribute names are returned
 *                                      if 1 the ids are used instead of the full attribute names
 *                                      if 2 the ids AND the full attribute name is returned
 * @param {String} [pUseIdValues=false] if true the values are not resolved or formatted [attributeId, attributeName, value]
 * 
 * @return {String[][]} two-dimensional array a row is [attributeId|attributeName, value] (or [attributeId, attributeName, value])
 */
AttributeRelationUtils.getAllAttributes = function (pObjectRowId, pObjectType, pUseAttributeIds, pUseIdValues)
{    
    var attributeQuery = new AttributeRelationQuery(pObjectRowId, pObjectType);
    
    if (!pUseAttributeIds || pUseAttributeIds == 2)
        attributeQuery.includeFullAttributeName();
    if (!pUseIdValues)
        attributeQuery.includeDisplayValue();
    
    return attributeQuery.getAttributes().map(function (row)
    {
        var value = pUseIdValues ? row.value : row.displayValue;
        switch (pUseAttributeIds)
        {
            case 1:
                return [row.attributeId, value];
            case 2:
                return [row.attributeId, row.fullAttributeName, value];
            case 0:
            default:
                return [row.fullAttributeName, value];
        }
    });
}

/**
 * gets the correct attribute value from a map with values depending on the attribute id
 * 
 * @param {String} pAttributeId the attribute id
 * @param {Object} pValueMap a map with the attribute values and the db fields as keys
 * @param {Boolean} [pGetViewValue=false] if true, get the view value
 * 
 * @return {String|null} the value of the attribute or null if the attribute doesn't exist
 */
AttributeRelationUtils.selectAttributeValue = function (pAttributeId, pValueMap, pGetViewValue)
{
    var type = newSelect("ATTRIBUTE_TYPE, DROPDOWNDEFINITION")
                    .from("AB_ATTRIBUTE")
                    .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId)
                    .arrayRow();
                    
    if (!type.length)
        return null;
    
    type[0] = type[0].trim();
    var field = AttributeTypeUtil.getDatabaseField(type[0]);
    var value = pValueMap[field];
    if(value == undefined)
        return "";
    if (pGetViewValue && type[0] == $AttributeTypes.COMBO)
    {
        value = newSelect("ATTRIBUTE_NAME")
                    .from("AB_ATTRIBUTE")
                    .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", value)
                    .cell();
    }
    else if (pGetViewValue)
        value = AttributeTypeUtil.getAttributeViewValue(type[0], value, type[1]);
    
    return value;
}

/**
 * @deprecated use AttributeRelationQuery.prototype.insertAttribute
 * 
 * Inserts an attribute relation and validates if it can be used for the context and 
 * it also heeds the max usage count.
 * 
 * @return {boolean} true, if the attribute relation was inserted
 */
AttributeRelationUtils.setAttribute = function (pRowId, pObjectType, pAttributeId, pValue)
{
    return new AttributeRelationQuery(pRowId, pAttributeId, pObjectType)
        .insertAttribute(pValue, false);
}

/**
 * @deprecated use AttributeRelationQuery.prototype.insertAttribute
 * 
 * inserts an attribute without validating the count
 */
AttributeRelationUtils.insertAttribute = function (pRowId, pObjectType, pAttributeId, pValue)
{
    return new AttributeRelationQuery(pRowId, pAttributeId, pObjectType)
        .insertAttribute(pValue, true);
}

/**
 * adds rows for attributes with min_count > 0
 * 
 * @param {String} pObjectType the object type
 * @param {String} pConsumer the name of the attribute relation consumer
 * @param {String[]} pFiltered array of attributeId's which act as a whitelist. (groups are resolves to the childid's)
 */
AttributeRelationUtils.presetMandatoryAttributes = function (pObjectType, pConsumer, pFiltered)
{
    var mandatoryAttributesSelect = newSelect("AB_ATTRIBUTE_ID, MIN_COUNT")
                                    .from("AB_ATTRIBUTEUSAGE")
                                    .join("AB_ATTRIBUTE", "AB_ATTRIBUTE_ID = AB_ATTRIBUTEID")
                                    .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)
                                    .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, SqlBuilder.NOT_EQUAL())
                                    .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.GROUP, SqlBuilder.NOT_EQUAL())
                                    .and("ATTRIBUTE_ACTIVE = 1")
                                    .and("MIN_COUNT > 0");
                                    
    if (pFiltered)
    {
        var possibleIds = AttributeUtil.getPossibleAttributes(pObjectType, false, pFiltered);
        if (possibleIds.length > 0)
            mandatoryAttributesSelect.and("AB_ATTRIBUTE.AB_ATTRIBUTEID", possibleIds, SqlBuilder.IN())
        else
            return;
    }

    var mandatoryAttributes = mandatoryAttributesSelect.table();
    mandatoryAttributes.forEach(function (usage)
    {
        //adding an attribute more than 20 times would be too much (having a min_count > 20 is very unlikely)
        for (let i = 0; i < usage[1] && i < 20; i++)
            neon.addRecord(pConsumer, {
                "AB_ATTRIBUTE_ID" : usage[0]
            });
    });
}

/**
 * clears rows of attribute
 * 
 * @param {String} pConsumer the name of the attribute relation consumer
 */
AttributeRelationUtils.clearAttributes = function (pConsumer)
{
    var insertedLinks = vars.get("$field." + pConsumer + ".insertedRows");
    var updatedLinks = vars.get("$field." + pConsumer + ".changedRows");
        
    insertedLinks.concat(updatedLinks).forEach(function (link)
    {
        if (link["AB_ATTRIBUTE_ID"])
            neon.deleteRecord(pConsumer, link["#UID"]);
    });
}

/**
 * Checks if the count of the used attributes is valid and returns a message if it's not.
 * 
 * @param {String} pRowId the row id of the entity
 * @param {String} [pObjectType=null] the object type
 * @param {String} pConsumerField the name of the attribute relation consumer
 * @param {String} [pFilteredAttributeIds] filters the attributes that are validated, this should
 *          be the same as the FilteredAttributeIds_param
 * 
 * @return {String} the validation message or an empty string if everything is ok
 */
AttributeRelationUtils.validateAttributeCount = function (pRowId, pObjectType, pConsumerField, pFilteredAttributeIds)
{
    var attributeChanges = {};
    var deletedRows = vars.get("$field." + pConsumerField + ".deletedRows");
    var changedRows = vars.get("$field." + pConsumerField + ".changedRows");
    var insertedRows = vars.get("$field." + pConsumerField + ".insertedRows");

   if (deletedRows)
    {
        deletedRows.forEach(function (row)
        {
            this[row.UID] = "";
        }, attributeChanges);
    }
    
    if (changedRows)
    {
        changedRows.forEach(function (row)
        {
            this[row.UID] = row.AB_ATTRIBUTE_ID;
        }, attributeChanges);
    }
    
    //get the current count of usages considering the changes
    //this will merge the counts of attributeChanges and the already stored attributerelations
    var countObj = AttributeRelationUtils.countAttributeRelations(pRowId, pObjectType, attributeChanges);
    
    if (insertedRows) //append the new rows
    {
        insertedRows.forEach(function (row)
        {
            this[row.AB_ATTRIBUTE_ID] = (this[row.AB_ATTRIBUTE_ID] || 0) + 1;
        }, countObj);
    }
    
    var possibleAttributes = AttributeUtil.getPossibleAttributes(pObjectType, undefined, pFilteredAttributeIds);
    var minMaxCounts = [];
    
    if (possibleAttributes.length > 0)
    {
        var minMaxCountsSelect = newSelect("AB_ATTRIBUTEID, ATTRIBUTE_NAME, MIN_COUNT, MAX_COUNT")
                                    .from("AB_ATTRIBUTEUSAGE")
                                    .join("AB_ATTRIBUTE", "AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                                    .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", possibleAttributes, SqlBuilder.IN())
                                    .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)

        //retrieve all min/max counts of the possible attributes
        minMaxCounts = minMaxCountsSelect.table();
    }
        
    var validationMessage = [];
    minMaxCounts.forEach(function ([attributeId, name, minCount, maxCount])
    {
        let count = this[attributeId] || 0;
        //compares the actual usage with the min and max count and generates a message if the usage is too low or too high
        if (count < minCount)
            validationMessage.push(translate.withArguments("Attribute \"%0\" has to be used at least %1.", [name, _getTranslatedCount(minCount)]));
        if (maxCount && count > maxCount)
            validationMessage.push(translate.withArguments("Attribute \"%0\" can't be used more than %1.", [name, _getTranslatedCount(maxCount)]));
    }, countObj);
    
    return validationMessage.join("\n");
    
    //returns the correct count expression by choosing either singular (1 time) or plural (2 times)
    function _getTranslatedCount (pCount)
    {
        if (pCount == 1)
            return pCount + " " + translate.text("${COUNT_PREPOSITION_SINGLE}");
        return pCount + " " + translate.text("${COUNT_PREPOSITION_MULTIPLE}");
    }
}

/**
 * counts attribute relations
 * 
 * @param {String} pRowId the row id of the entity
 * @param {String} [pObjectType=null] the object type
 * @param {Object} [pAttributeChanges=null] object containing changes and deletions of attributes
 *                          structure = {attributeRelationId : new attributeId or "" when deleted}
 * 
 * @return {Object} object with attribute ids and the count of the usage (without new rows)
 */
AttributeRelationUtils.countAttributeRelations = function (pRowId, pObjectType, pAttributeChanges)
{
    //use cases:
    //complete new row ==> increase count by 1 for that attribute [this is done in another function]
    //row removed ==> decrease count by 1
    //row edit: replace one attribute by another attribute ==> increase the new attribute count and decrease the old attribute count
    //row edit: replace attribute with no new value ==> decrease count for the old attribute
    //unchanged (already stored) row ==> increase count 
    var countObj = {};

    storedAttributeRelations = new AttributeRelationQuery()
        .objectRowId(pRowId)
        .objectType(pObjectType)
        .getAttributes();
                        
    storedAttributeRelations.forEach(function (storedRow) {
        var storedAttributeId = storedRow.attributeId;
        var storedAttrRelationId = storedRow.attributeRelationId;
        var currentAttributeId = storedAttributeId;
        //merging the data that is stored in the DB and the provided changes
        if (pAttributeChanges && storedAttrRelationId in pAttributeChanges)
            currentAttributeId = pAttributeChanges[storedAttrRelationId];
        
        // it doesn't matter if a row has been deleted or if the attribute has been set to "nothing"
        if (currentAttributeId == "")
            _decrCount(storedAttributeId);
        else
        {
            _incrCount(currentAttributeId);
            if (currentAttributeId != storedAttributeId)
                _decrCount(storedAttributeId);
        }
    });
    
    function _incrCount(pAttributeId)
    {
        if (countObj[pAttributeId])
            countObj[pAttributeId]++;
        else
            countObj[pAttributeId] = 1;
    }
    
    function _decrCount(pAttributeId)
    {
        if (countObj[pAttributeId])
            countObj[pAttributeId]--;
        else
            countObj[pAttributeId] = 0;
    }
    
    return countObj;
}

/*********************************************************************************************************************/


/**
 * Object for the enumeration and management of all attribute types.
 * This Object is only for the general definition of attribute types and for getting
 * data about every type, anything that has to do with a specific attribute (= the function requires an attribute id)
 * should be done in AttributeUtils.
 * The required values and methods for each type are:
 * 
 * toString = function that should return a unique name
 * contentType = the value that is returned in the contentType process for the attribute
 * databaseField = the database field that holds values of attributes with the type
 * 
 * optional:
 * getViewValue = function that gets the display value for a given value
 * isGroup = if true, the attribute can have children
 * getDropDownDefinitions = function that returns an array of possible values
 *          for DROPDOWNDEFINITION
 * singleSelection = if true, the maximal usage count is always 1
 * 
 * The display name is controlled by the keyword 'AttributeType'
 */
function $AttributeTypes () {}

$AttributeTypes.TEXT = { 
    toString : function () {return "TEXT";},
    contentType : "TEXT", 
    databaseField : "CHAR_VALUE"
};
$AttributeTypes.DATE = {
    toString : function () {return "DATE";},
    contentType : "DATE", 
    databaseField : "DATE_VALUE",
    getViewValue : function (pValue)
        {
            return datetime.toDate(pValue, translate.text("dd.MM.yyyy"));
        }
};
$AttributeTypes.NUMBER = {
    toString : function () {return  "NUMBER";},
    contentType : "NUMBER", 
    databaseField : "NUMBER_VALUE"
};
$AttributeTypes.BOOLEAN = {
    toString : function () {return "BOOLEAN";},
    contentType : "BOOLEAN", 
    databaseField : "INT_VALUE",
    singleSelection : true,
    getViewValue : function (pValue)
        {
            return pValue == "1" ? translate.text("Yes") : translate.text("No");
        }
};
$AttributeTypes.COMBO = {
    toString : function () {return "COMBO";},
    contentType : "UNKNOWN",
    databaseField : "ID_VALUE",
    isGroup : true,
    possibleChildren : ["COMBOVALUE"],
    //in most cases the view value of this attribute type is loaded via a direct sql join for less queries and better performance
    getViewValue : function (pValue)
        {
            var viewValue = newSelect("AB_ATTRIBUTE.ATTRIBUTE_NAME")
                .from("AB_ATTRIBUTE")
                .where("AB_ATTRIBUTE.AB_ATTRIBUTEID", pValue)
                .and("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE)
                .cell();
            return viewValue ? translate.text(viewValue) : viewValue;
        }
};
$AttributeTypes.COMBOVALUE = {
    toString : function () {return "COMBOVALUE";},
    contentType : null, 
    databaseField : null
};
$AttributeTypes.GROUP = {
    toString : function () {return "GROUP";},
    contentType : null, 
    databaseField : null,
    isGroup : true,
    possibleChildren : ["GROUP", "TEXT", "NUMBER", "COMBO", "VOID", "THEME", "KEYWORD", "OBJECTSELECTION", "MEMO", "DATE", "BOOLEAN"]
};
$AttributeTypes.KEYWORD = {
    toString : function () {return "KEYWORD";},
    contentType : "UNKNOWN", 
    databaseField : "ID_VALUE", 
    getViewValue : function (pValue, pKeyword)
        {
            return KeywordUtils.getViewValue(pKeyword, pValue);
        },
    getDropDownDefinitions : function ()
        {
            return KeywordUtils.getContainerNames().map(function (e)
            {
                return [e, e];//currently the first column is ID, second view value - which is the same because there is no ID for keyword-containers
            });
        }
};
$AttributeTypes.VOID = {
    toString : function () {return "VOID";},
    contentType : null,
    databaseField : null,
    isGroup : true,
    possibleChildren : ["VOID"],
    singleSelection : true
};
$AttributeTypes.MEMO = { 
    toString : function () {return "MEMO";},
    contentType : "LONG_TEXT", 
    databaseField : "CHAR_VALUE"
};
$AttributeTypes.OBJECTSELECTION = {
    toString : function () {return "OBJECTSELECTION";},
    contentType : "UNKNOWN",
    databaseField : "ID_VALUE",
    getViewValue : function (pValue, pModule)
        {
            if (pValue)
            {
                if (pModule == "Employee_entity")
                    pValue = EmployeeUtils.prefixUserId(pValue);
                
                var title = "#CONTENTTITLE";
                var config = entities.createConfigForLoadingRows()
                    .entity(pModule)
                    .fields([title])
                    .uid(pValue);
                var rows = entities.getRow(config);
                pValue = rows ? rows[title] : pValue;
            }
            return pValue;
        },
    getDropDownDefinitions : function ()
        {
            // TODO: use loadEntity from context_entity
            var dropDownList = [];
            project.getDataModels(project.DATAMODEL_KIND_ENTITY).forEach(
                function (entity)
                {
                    if (entity[1] && $AttributeTypes.OBJECTSELECTION._selectableEntities[entity[0]])
                        dropDownList.push([entity[0], translate.text(entity[1])]);
                }
            );
            return dropDownList;
        },
    /** @private */
    _selectableEntities : {
        "ObjectRelationType_entity" : true,
        "DocumentTemplate_entity" : true,
        "SupportTicket_entity" : true,
        "Organisation_entity" : true,
        "Salesproject_entity" : true,
        "Productprice_entity" : true,
        "SerialLetter_entity" : true,
        "AnyContact_entity" : true,
        "Salutation_entity" : true,
        "Attribute_entity" : true,
        "Activity_entity" : true,
        "Contract_entity" : true,
        "Campaign_entity" : true,
        "BulkMail_entity" : true,
        "Employee_entity" : true,
        "Language_entity" : true,
        "Product_entity" : true,
        "Person_entity" : true,
        "Offer_entity" : true,
        "Order_entity" : true,
        "Task_entity" : true,
        "Role_entity" : true
    }
};
$AttributeTypes.THEME = {
    toString : function () {return "THEME";},
    contentType : "LONG_TEXT",
    databaseField : "CHAR_VALUE",
    isGroup : true,
    possibleChildren : ["THEME"]
};

function AttributeTypeUtil () {}

/**
 * returns the required contentType for the given attribute type
 * 
 * @param {String} pAttributeType the attribute type 
 *                  (use the values of the AttributeTypes object, e. g. AttributeTypes.TEXT)
 * @return {String} the contentType for the attribute
 */
AttributeTypeUtil.getContentType = function (pAttributeType)
{
    return AttributeTypeUtil._getProperty(pAttributeType, "contentType");
}

/**
 * returns if the type is a group type
 * 
 * @param {String} pAttributeType the attribute type 
 *                  (use the values of the AttributeTypes object, e. g. AttributeTypes.TEXT)
 * @return {Boolean} if the type is a group type
 */
AttributeTypeUtil.isGroupType = function (pAttributeType)
{
    return AttributeTypeUtil._getProperty(pAttributeType, "isGroup", false);
}

/**
 * returns the database field for the given attribute type that holds the value of the attribute
 * 
 * @param {String} pAttributeType the attribute type 
 *                  (use the values of the AttributeTypes object, e. g. AttributeTypes.TEXT)
 * @return {String} the database field for the attribute
 */
AttributeTypeUtil.getDatabaseField = function (pAttributeType)
{
    return AttributeTypeUtil._getProperty(pAttributeType, "databaseField");
}

/**
 * returns the possible children types for the given attribute type
 * 
 * @param {String} pAttributeType the attribute type 
 *                  (use the values of the AttributeTypes object, e. g. AttributeTypes.TEXT)
 * @return {String[]} the possible children types
 */
AttributeTypeUtil.getPossibleChildren = function (pAttributeType)
{
    return AttributeTypeUtil._getProperty(pAttributeType, "possibleChildren");
}

/**
 * returns the possible children types for the given attribute type
 * 
 * @param {String} pAttributeType the attribute type 
 *                  (use the values of the AttributeTypes object, e. g. AttributeTypes.TEXT)
 * @return {String[]} the possible children types
 */
AttributeTypeUtil.isSingleSelection = function (pAttributeType)
{
    return AttributeTypeUtil._getProperty(pAttributeType, "singleSelection", false);
}

AttributeTypeUtil.useLookup = function (pAttributeType)
{
    return pAttributeType.trim() == $AttributeTypes.OBJECTSELECTION.toString();
}

AttributeTypeUtil.getGroupTypes = function (pChildType)
{
    var groupTypes = [];
    for (let type in $AttributeTypes)
    {
        if (AttributeTypeUtil.isGroupType(type) && (!pChildType || (!AttributeTypeUtil.getPossibleChildren(type) || AttributeTypeUtil.getPossibleChildren(type).indexOf(pChildType) !== -1)) )
            groupTypes.push(type);
    }
    return groupTypes;
}

/**
 * function to get a property of an attribute type
 */
AttributeTypeUtil._getProperty = function (pAttributeType, pPropertyName, pDefaultValue)
{
    if (!pAttributeType)
        return null;
    
    pAttributeType = pAttributeType.trim();
    if (pAttributeType in $AttributeTypes)
        if (pPropertyName in $AttributeTypes[pAttributeType])
            return $AttributeTypes[pAttributeType][pPropertyName];
        else
            return pDefaultValue === undefined ? null : pDefaultValue;
        
    return null;
}

AttributeTypeUtil.getAttributeViewValue = function (pAttributeType, pValue, pKeyword)
{
    if (pAttributeType in $AttributeTypes && $AttributeTypes[pAttributeType].getViewValue)
        return $AttributeTypes[pAttributeType].getViewValue(pValue, pKeyword);
    return pValue;
}

AttributeTypeUtil._initTypeColumnData = function ()
{
    var columns = [];
    var typeColumnMap = {};
    for (let type in $AttributeTypes)
    {
        type = $AttributeTypes[type];
        if (type.databaseField)
        {
            var typeKey = type.toString();
            var colIndex = columns.indexOf(type.databaseField);
            if (colIndex == -1)
            {
                colIndex = columns.length;
                columns.push(type.databaseField);
            }
            typeColumnMap[typeKey] = colIndex;
        }
    }
    this._allDBColumns = columns;
    this._typeColumnMap = typeColumnMap;
}

AttributeTypeUtil.getAllDatabaseFields = function ()
{
    if (this._allDBColumns == undefined)
        AttributeTypeUtil._initTypeColumnData();
    return this._allDBColumns;
}

AttributeTypeUtil.getTypeColumnIndex = function (pAttributeType)
{
    if (this._typeColumnMap == undefined)
        AttributeTypeUtil._initTypeColumnData();
    return this._typeColumnMap[pAttributeType.trim()];
}

/*********************************************************************************************************************/

/**
 * Functions for AttributeUsages.
 * Do not instanciate this!
 */
function AttributeUsageUtil () {}

/**
 * Creates AttributeUsages for all subordinate attributes of an attribute.
 * This is required when an usage is added to a superordinate attribute.
 * 
 * @param {String} pAttributeId the id of the superordinate attribute
 * @param {String} pObjectType the context
 */
AttributeUsageUtil.insertChildrenUsages = function (pAttributeId, pObjectType)
{
    if (!pAttributeId)
        return;
    var table = "AB_ATTRIBUTEUSAGE";
    var columns = ["AB_ATTRIBUTEUSAGEID", "AB_ATTRIBUTE_ID", "OBJECT_TYPE", "MAX_COUNT"];
    var types = db.getColumnTypes(table, columns);
    
    var inserts = [];
    _addInserts(pAttributeId, pObjectType);
    db.inserts(inserts);
    
    function _addInserts (pAttributeId, pObjectType)
    {
        var attributes = newSelect(["AB_ATTRIBUTEID", "ATTRIBUTE_TYPE",
                                newSelect("count(*)")
                                    .from("AB_ATTRIBUTEUSAGE")
                                    .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID")
                                    .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType)])
                            .from("AB_ATTRIBUTE")
                            .where("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, SqlBuilder.NOT_EQUAL())
                            .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId)
                            .table();

        attributes.forEach(function (row)
        {
            if (row[2] == "0")
            {
                let maxCount = AttributeTypeUtil.isSingleSelection(row[1])
                    ? "1"
                    : "";
                let values = [util.getNewUUID(), row[0], pObjectType, maxCount];
                inserts.push([table, columns, types, values]);
            }
            _addInserts(row[0], pObjectType);
        });
    }
}

/**
 * Updates AttributeUsages for all subordinate attributes of an attribute.
 * This is required when an usage of a superordinate attribute is changed.
 * 
 * @param {String} pAttributeId the id of the superordinate attribute
 * @param {String} pOldObjectType ye olde context
 * @param {String} pNewObjectType the new context
 */
AttributeUsageUtil.updateChildrenUsages = function (pAttributeId, pOldObjectType, pNewObjectType)
{
    if (!pNewObjectType || !pAttributeId || !pOldObjectType)
        return;
    
    var table = "AB_ATTRIBUTEUSAGE";
    
    var countSubQuery = newSelect("count(*)")
                            .from("AB_ATTRIBUTEUSAGE")
                            .where("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pNewObjectType)
                            .and("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID");
    
    var sqlSelect = newSelect(["AB_ATTRIBUTEID", "AB_ATTRIBUTEUSAGEID", countSubQuery])
                        .from("AB_ATTRIBUTE")
                        .leftJoin("AB_ATTRIBUTEUSAGE", newWhere()
                                .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pOldObjectType)
                                .and("AB_ATTRIBUTEID = AB_ATTRIBUTE_ID"));
    
    var updateCond = newWhere();
    
    //it is possible that the new objectType is already in a subordinate attribute 
    //and an update could cause a duplicate entry so one has to be deleted
    var deleteCond = newWhere();
    
    _addUpdateIds(pAttributeId, pOldObjectType);
        
    updateCond.updateData(true, table, ["OBJECT_TYPE"], null, [pNewObjectType]);
    deleteCond.deleteData(true, table);
    
    function _addUpdateIds (pAttributeId)
    {
        sqlSelect.clearWhere()
                 .where("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, SqlBuilder.NOT_EQUAL())
                 .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId);

        var attributes = sqlSelect.table();
        
        attributes.forEach(function (row)
        {
            if (row[1] && row[2] != "0")
                deleteCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1]);
            else if (row[1])
                updateCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1]);
            _addUpdateIds(row[0]);
        });
    }
}

/**
 * Deletes AttributeUsages for all subordinate attributes of an attribute.
 * This is required when an usage is removed from a superordinate attribute.
 * 
 * @param {String} pAttributeId the id of the superordinate attribute
 * @param {String} pObjectType the context
 */
AttributeUsageUtil.deleteChildrenUsages = function (pAttributeId, pObjectType)
{
    var attributeSelect = newSelect("AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID")
                            .from("AB_ATTRIBUTE")
                            .leftJoin("AB_ATTRIBUTEUSAGE", newWhere("AB_ATTRIBUTEID = AB_ATTRIBUTE_ID")
                                                               .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType));
    
    var deleteCond = newWhere().from("AB_ATTRIBUTEUSAGE");
    _addDeleteIds(pAttributeId, pObjectType);
    
    deleteCond.deleteData();
    
    function _addDeleteIds (pAttributeId)
    {
        attributeSelect.clearWhere()
                       .where("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, SqlBuilder.NOT_EQUAL())
                       .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId);
        var attributes = attributeSelect.table();
        
        attributes.forEach(function (row)
        {
            if (row[1])
                deleteCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1])
            _addDeleteIds(row[0]);
        });
    }
}

/**
 * Deletes duplicate attribute usages.
 * 
 * @param {String} [pAttributeId=null] attribute id, if omitted, all duplicates will be deleted 
 */
AttributeUsageUtil.removeDuplicates = function (pAttributeId)
{
    
    
    var attributeSelect = newSelect("AB_ATTRIBUTEUSAGEID, AB_ATTRIBUTE_ID, OBJECT_TYPE")
                                .from("AB_ATTRIBUTEUSAGE")
                                .where(null, newSelect("AB_ATTRIBUTEUSAGEID")
                                                .from("AB_ATTRIBUTEUSAGE", "USAGEDUP")
                                                .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = USAGEDUP.AB_ATTRIBUTE_ID")
                                                .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE = USAGEDUP.OBJECT_TYPE")
                                                .and("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID != USAGEDUP.AB_ATTRIBUTEUSAGEID"), 
                                        SqlBuilder.EXISTS());
                                        
    attributeSelect.andIfSet("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", pAttributeId);
    
    var duplicates = attributeSelect.table();
    var usageObj = {};
    var deleteCond = newWhere().from("AB_ATTRIBUTEUSAGE");
    
    duplicates.forEach(function (row)
    {
        if (!(row[1] in this))
            this[row[1]] = {};
        if (row[2] in this[row[1]])
            deleteCond.or("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[0]);
        this[row[1]][row[2]] = true;
    }, usageObj);
    
    deleteCond.deleteData();
}

/*************************************************************************************************/

/**
 * @class
 * 
 * An AttributeRelationQuery can be used for getting the value and other properties
 * of an AttributeRelation. You have to instanciate it with "new".
 * 
 * This is built like this because there are several different scenarios for
 * loading the values or other properties of one or more attribute relations. Because of this,
 * the constructor takes in the common parameters for loading attribute relations and you can
 * use methods of the constructed object to configure the query and get the desired result.
 */
function AttributeRelationQuery (pObjectRowId, pAttributeId, pObjectType)
{
    this._rowId = pObjectRowId || null;
    this._objectType = pObjectType || null;
    this._attributeIds = pAttributeId ? [pAttributeId] : null;
    this._attributeTypes = null;
    this._includeFullAttributeName = false;
    this._includeDisplayValue = false;
    
    return this;
}

/**
 * sets the object-row-id for the query
 */
AttributeRelationQuery.prototype.objectRowId = function (pObjectRowId)
{
    this._rowId = pObjectRowId;
    return this;
}

/**
 * sets the object-type for the query
 */
AttributeRelationQuery.prototype.objectType = function (pObjectType)
{
    this._objectType = pObjectType;
    return this;
}

/**
 * sets the attribute id for the query
 */
AttributeRelationQuery.prototype.attributeId = function (pAttributeId)
{
    this._attributeIds = [pAttributeId];
    return this;
}

/**
 * sets the attribute ids for the query
 */
AttributeRelationQuery.prototype.attributeIds = function (pAttributeIds)
{
    this._attributeIds = pAttributeIds;
    return this;
}

/**
 * sets the attribute type for the query
 */
AttributeRelationQuery.prototype.attributeTypes = function (pAttributeTypes)
{
    this._attributeTypes = pAttributeTypes;
    return this;
}

/**
 * if this method was called, the query result will contain the fullAttributeName
 */
AttributeRelationQuery.prototype.includeFullAttributeName = function ()
{
    this._includeFullAttributeName = true;
    return this;
}

/**
 * if this method was called, the query result will contain the displayValue
 */
AttributeRelationQuery.prototype.includeDisplayValue = function ()
{
    this._includeDisplayValue = true;
    return this;
}

/**
 * Executes the query and returns the result, depending on the properties of the AttributeRelationQuery object.
 * 
 * @return {AttributeRelation[]} Array of objects. By default, the objects contain the properties {attributeId, value, attributeRelationId, attributeName, attributeType}.
 *      If includeDisplayValue is true, the object also contains the property 'displayValue' and if includeFullAttributeName is true, there is also the property
 *      'fullAttributeName'.
 */
AttributeRelationQuery.prototype.getAttributes = function ()
{
    var defaultFields = [
        "AB_ATTRIBUTE.ATTRIBUTE_TYPE", 
        "AB_ATTRIBUTE.DROPDOWNDEFINITION", 
        "AB_ATTRIBUTE.ATTRIBUTE_NAME",
        "COMBOVAL.ATTRIBUTE_NAME",
        "AB_ATTRIBUTE.AB_ATTRIBUTEID",
        "AB_ATTRIBUTERELATION.AB_ATTRIBUTERELATIONID",
        "AB_ATTRIBUTERELATION.OBJECT_ROWID",
        "AB_ATTRIBUTERELATION.OBJECT_TYPE"
    ];
    
    var valueFields = AttributeTypeUtil.getAllDatabaseFields();

    var attributeValues = AttributeRelationUtils.getAttributeSqlBuilder(defaultFields.concat(valueFields), this._rowId, this._objectType)
        .andIfSet("AB_ATTRIBUTERELATION.AB_ATTRIBUTE_ID", this._attributeIds, SqlBuilder.IN())
        .andIfSet("AB_ATTRIBUTE.ATTRIBUTE_TYPE", this._attributeTypes, SqlBuilder.IN())
        .table();
        
    if (attributeValues.length == 0)
        return [];
    
    var mappingFn = function (row)
    {
        var attrObj = new AttributeRelation(row[5], row[4], row[AttributeTypeUtil.getTypeColumnIndex(row[0]) + defaultFields.length], row[2], row[0], row[6], row[7]);
        
        if (this._includeDisplayValue)
        {
            if (row[0].trim() == $AttributeTypes.COMBO)
                attrObj.displayValue = translate.text(row[3]);
            else
                attrObj.displayValue = AttributeTypeUtil.getAttributeViewValue(row[0].trim(), attrObj.value, row[1]);
        }
        if (this._includeFullAttributeName)
        {
            attrObj.fullAttributeName = AttributeUtil.getFullAttributeName(row[4]);
        }
        
        return attrObj;
    }
    
    return attributeValues.map(mappingFn, this);
}

/**
 * @return {AttributeRelation}
 */
AttributeRelationQuery.prototype.getSingleAttribute = function ()
{
    if (!this._attributeIds || this._attributeIds.length !== 1)
        throw new Error("You have to specify a single attribute id");
    return this.getAttributes()[0] || null;
}

/**
 * Executes the query and returns a single value. For this, there must be a attribute id set.
 * 
 * @return {String} 
 */
AttributeRelationQuery.prototype.getSingleAttributeValue = function ()
{
    var attribute = this.getSingleAttribute();
    return attribute ? attribute.value : null;
}

/**
 * Executes the query and returns the count of datasets.
 * 
 * @return {Number} the number of attribute relations 
 */
AttributeRelationQuery.prototype.getAttributeCount = function ()
{
    return parseInt(AttributeRelationUtils.getAttributeSqlBuilder("count(*)", this._rowId, this._objectType)
        .andIfSet("AB_ATTRIBUTERELATION.AB_ATTRIBUTE_ID", this._attributeIds, SqlBuilder.IN())
        .cell() || 0);
}

/**
 * inserts a new attribute relation
 * 
 * @param {String} pValue value of the attribute relation
 * @param {boolean} [pOmitValidation=false] if set to true, the current usage of the attribute and max count won't be checked
 * 
 * @return {boolean} true, if the attribute relation was inserted, false if the count validation failed
 */
AttributeRelationQuery.prototype.insertAttribute = function (pValue, pOmitValidation)
{
    if (!this._objectType || !this._rowId)
        throw new Error("AttributeRelationQuery: Object type and row id are required for insert");
    if (!this._attributeIds || this._attributeIds.length !== 1)
        throw new Error("AttributeRelationQuery: You have to specify a single attribute id for insert");
    
    var attributeId = this._attributeIds[0];
    
    if (!pOmitValidation)
    {
        var maxCount = newSelect("MAX_COUNT")
            .from("AB_ATTRIBUTEUSAGE")
            .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId)
            .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", this._objectType)
            .arrayColumn();

        if (maxCount.length == 0)
            return false;

        maxCount = maxCount[0];
        if (maxCount && maxCount != 0)
        {
            let timesUsed = this.getAttributeCount();
            if (timesUsed >= maxCount)
                return false;
        }
    }

    var attrData = {
        "AB_ATTRIBUTE_ID" : attributeId,
        "OBJECT_ROWID" : this._rowId,
        "OBJECT_TYPE" : this._objectType,
        "DATE_NEW" : vars.get("$sys.date"),
        "USER_NEW" : vars.get("$sys.user")
    };
    var type = AttributeUtil.getAttributeType(attributeId);
    var valueField = AttributeTypeUtil.getDatabaseField(type);
    if (valueField)
        attrData[valueField] = pValue;
    
    new SqlBuilder().insertFields(attrData, "AB_ATTRIBUTERELATION", "AB_ATTRIBUTERELATIONID");
    return true;
}

/**
 * deletes all attribute relations with the given rowId and objectType
 * 
 * @return {Number} count of deleted rows
 */
AttributeRelationQuery.prototype.deleteAllAttributes = function ()
{
    if (!this._rowId)
        throw new Error("AttributeRelationQuery: Row id is required for delete");
    
    return newWhere("AB_ATTRIBUTERELATION.OBJECT_ROWID", this._rowId)
        .andIfSet("AB_ATTRIBUTERELATION.OBJECT_TYPE", this._objectType)
        .deleteData();
}

/**
 * Object representing one attribute relation in the database. Don't use this constructor in you own code!
 * Instances of this should only be created by functions in this library.
 * 
 * @param {String} pAttributeRelationId attribute relation id
 * @param {String} pAttributeId attribute id
 * @param {String} pValue value of the attribute
 * @param {String} pAttributeName name of the attribute
 * @param {String} pAttributeType type of the attribute
 * @param {String} pObjectRowId rowId of the linked object
 * @param {String} pObjectType context of the linked object
 */
function AttributeRelation (pAttributeRelationId, pAttributeId, pValue, pAttributeName, pAttributeType, pObjectRowId, pObjectType)
{
    if (!pAttributeRelationId)
        throw new Error("AttributeRelation: pAttributeRelationId must be provided");
        
    this.attributeRelationId = pAttributeRelationId;
    this.attributeId = pAttributeId;
    this.value = pValue;
    this.attributeName = pAttributeName;
    this.attributeType = pAttributeType;
    this.objectRowId = pObjectRowId;
    this.objectType = pObjectType;
    this.displayValue = undefined;
    this.fullAttributeName = undefined;
}

/**
 * updates the value of the attribute in the database
 * 
 * @param {String} pValue the new value of the attribute relation
 * @return {Boolean} currently the function always returns true (if some kind of validation is implemented in the future, 
 *      it will return false if the validation fails)
 */
AttributeRelation.prototype.updateAttribute = function (pValue)
{
    if (pValue == undefined || pValue == "")
        throw new Error("AttributeRelation: no value provided for update");
        
    var attrData = {
        "DATE_EDIT" : vars.get("$sys.date"),
        "USER_EDIT" : vars.get("$sys.user")
    };

    var valueField = AttributeTypeUtil.getDatabaseField(this.attributeType);
    if (valueField)
        attrData[valueField] = pValue;

    newWhere("AB_ATTRIBUTERELATION.AB_ATTRIBUTERELATIONID", this.attributeRelationId)
        .updateFields(attrData);
    return true;
}

/**
 * deletes the attribute relation from the database
 * 
 * @param {Boolean} [pOmitValidation=false] if set to true, the function won't check if the min count prohibits the deletion
 * @retun {Boolean} true if it was deleted and false if the min count doesn't allow the deletion
 */
AttributeRelation.prototype.deleteAttribute = function (pOmitValidation)
{
    if (!pOmitValidation)
    {
        var minCount = newSelect("MIN_COUNT")
            .from("AB_ATTRIBUTEUSAGE")
            .where("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", this.attributeId)
            .and("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", this.objectType)
            .cell();

        if (minCount && minCount != 0)
        {
            let timesUsed = new AttributeRelationQuery(this.objectRowId, this.attributeId, this.objectType).getAttributeCount();
            if (timesUsed <= minCount)
                return false;
        }
    }
    
    newWhere("AB_ATTRIBUTERELATION.AB_ATTRIBUTERELATIONID", this.attributeRelationId)
        .deleteData();
    return true;
}