diff --git a/process/Attribute_lib/process.js b/process/Attribute_lib/process.js index a7c705b9d04d5a254eaa7baa4196eb5b0cf388fc..488b7d7d594b4e979d0351441071cd4f304ee85e 100644 --- a/process/Attribute_lib/process.js +++ b/process/Attribute_lib/process.js @@ -1,829 +1,873 @@ -import("system.util"); -import("system.datetime"); -import("system.translate"); -import("system.neon"); -import("system.vars"); -import("system.db"); -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 () {} - -/** - * 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 attrSql = "select AB_ATTRIBUTEID from AB_ATTRIBUTE" - + " join AB_ATTRIBUTEUSAGE on AB_ATTRIBUTEID = AB_ATTRIBUTE_ID"; - var attrCond = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType) - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# <> ?") - .and("ATTRIBUTE_ACTIVE = 1"); - - if (pAttributeCount) - { - for (let attributeId in pAttributeCount) - { - attrCond.andSqlCondition( - SqlCondition.begin() - .orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId, "# != ?") - .orPrepare("AB_ATTRIBUTEUSAGE.MAX_COUNT", pAttributeCount[attributeId], "# > ?") - .or("AB_ATTRIBUTEUSAGE.MAX_COUNT is null") - ); - } - } - - if (pFilteredAttributeIds) - { - var filteredIdsCondition = new SqlCondition(); - var filteredIdChildren = AttributeUtil.getAllChildren(pFilteredAttributeIds); - pFilteredAttributeIds.concat(filteredIdChildren).forEach(function(id) - { - this.orPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", id); - }, filteredIdsCondition); - - attrCond.andSqlCondition(filteredIdsCondition); - } - - if (!pIncludeGroups) - attrCond.and("ATTRIBUTE_TYPE != '" + $AttributeTypes.GROUP + "'"); - - var attributes = db.array(db.COLUMN, attrCond.buildSql(attrSql)); - - return attributes; -} - -/** - * 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. - * - * @return {String} the name of the attribute - */ -AttributeUtil.getFullAttributeName = function (pAttributeId, pSimpleName) -{ - if (pSimpleName == undefined) - pSimpleName = false; - - if (!pAttributeId) - return ""; - if (pSimpleName) - return AttributeUtil.getSimpleAttributeName(pAttributeId); - var attributeNames = []; - var attribute; - do { - attribute = db.array(db.ROW, SqlCondition.begin() - .andPrepare(["AB_ATTRIBUTE", "AB_ATTRIBUTEID", "ATTRIBUTE"], pAttributeId) - .buildSql("select ATTRIBUTE.ATTRIBUTE_NAME, PARENT1.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_PARENT_ID \n\ - from AB_ATTRIBUTE ATTRIBUTE \n\ - left join AB_ATTRIBUTE PARENT1 on ATTRIBUTE.ATTRIBUTE_PARENT_ID = PARENT1.AB_ATTRIBUTEID \n\ - left join AB_ATTRIBUTE PARENT2 on PARENT1.ATTRIBUTE_PARENT_ID = PARENT2.AB_ATTRIBUTEID") - ); - if (attribute.length > 0) - { - attributeNames.push(attribute[0]); - if (attribute[1]) - attributeNames.push(attribute[1]); - if (attribute[2]) - attributeNames.push(attribute[2]); - pAttributeId = attribute[3]; - } - else - pAttributeId = ""; - } while (pAttributeId); - - return attributeNames.reverse().join(" / "); -} - -/** - * returns the name of an attribute - * - * @param {String} pAttributeId the id of the attribute - * - * @return {String} the name of the attribute - */ -AttributeUtil.getSimpleAttributeName = function (pAttributeId) -{ - var attributeName = db.cell(SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) - .buildSql("select ATTRIBUTE_NAME from AB_ATTRIBUTE") - ); - - 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 = db.array(db.COLUMN, SqlCondition.begin() - .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID in ('" + pAttributeIds.join("','") + "')") - .buildSql("select AB_ATTRIBUTEID from AB_ATTRIBUTE") - ); - 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 db.cell(SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) - .buildSql( - "select exists (" - + "select AB_ATTRIBUTERELATIONID from AB_ATTRIBUTERELATION " - + "where AB_ATTRIBUTE_ID = AB_ATTRIBUTEID" - + ") from AB_ATTRIBUTE", "1=2" - ) - ) == "true"; -} - -/** - * returns the type of an attribute - * - * @param {String} pAttributeId the id of the attribute - * - * @result {String} attribute type - */ -AttributeUtil.getAttributeType = function (pAttributeId) -{ - var attrTypeSelect = "select ATTRIBUTE_TYPE from AB_ATTRIBUTE"; - attrTypeSelect = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) - .buildSql(attrTypeSelect); - return db.cell(attrTypeSelect).trim(); -} - -/*********************************************************************************************************************/ - -/** - * Provides functions for the work with attributeRelations, getting the value of an attributeRelation for an object. - * Don't instanciate this! - * - * @class - */ -function AttributeRelationUtils () {} - -/** - * 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 - * - * @return {String|null} the value of the attribute - */ -AttributeRelationUtils.getAttribute = function (pAttributeId, pObjectRowId, pObjectType, pGetViewValue) -{ - var attrCond = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTERELATION.OBJECT_ROWID", pObjectRowId) - .andPrepare("AB_ATTRIBUTERELATION.AB_ATTRIBUTE_ID", pAttributeId); - if (pObjectType != null) - attrCond.andPrepare("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType); - - var defaultFields = [ - "AB_ATTRIBUTE.ATTRIBUTE_TYPE", - "AB_ATTRIBUTE.KEYWORD_CONTAINER", - "COMBOVAL.ATTRIBUTE_NAME" - ]; - var valueFields = AttributeTypeUtil.getAllDatabaseFields(); - var attributeSql = attrCond.buildSql("select " + defaultFields.join(", ") + ", " + valueFields.join(", ") - + " from AB_ATTRIBUTERELATION join AB_ATTRIBUTE on AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID" - + " left join AB_ATTRIBUTE COMBOVAL on " + $AttributeTypes.COMBO.databaseField + " = COMBOVAL.AB_ATTRIBUTEID"); - - var attributeValues = db.array(db.ROW, attributeSql); - if (!attributeValues.length) - return null; - - let value = attributeValues[AttributeTypeUtil.getTypeColumnIndex(attributeValues[0]) + defaultFields.length]; - if (pGetViewValue && attributeValues[1].trim() == $AttributeTypes.COMBO) - value = attributeValues[2]; - else if (pGetViewValue) - value = AttributeTypeUtil.getAttributeViewValue(attributeValues[0].trim(), value, attributeValues[1]); - - return value; -} - -/** - * gets all attributes for a dataset - * - * @param {String} pObjectRowId object rowid - * @param {String} [pObjectType=null] object-type - * @param {String} [pUseAttributeIds=false] if true the ids are used instead of the full attribute names - * @param {String} [pUseIdValues=false] if true the values are not resolved or formatted - * - * @return {String[][]} two-dimensional array a row is [attributeId|attributeName, value] - */ -AttributeRelationUtils.getAllAttributes = function (pObjectRowId, pObjectType, pUseAttributeIds, pUseIdValues) -{ - var attrCond = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTERELATION.OBJECT_ROWID", pObjectRowId); - if (pObjectType != null) - attrCond.andPrepare("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType); - - var defaultFields = [ - "AB_ATTRIBUTE_ID", - "AB_ATTRIBUTE.ATTRIBUTE_TYPE", - "AB_ATTRIBUTE.KEYWORD_CONTAINER", - "COMBOVAL.ATTRIBUTE_NAME" - ]; - var valueFields = AttributeTypeUtil.getAllDatabaseFields(); - var attributeSql = attrCond.buildSql("select " + defaultFields.join(", ") + ", " + valueFields.join(", ") - + " from AB_ATTRIBUTERELATION join AB_ATTRIBUTE on AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID" - + " left join AB_ATTRIBUTE COMBOVAL on " + $AttributeTypes.COMBO.databaseField + " = COMBOVAL.AB_ATTRIBUTEID"); - - var attributeNameMap = {}; - var attributeValues = db.table(attributeSql).map(function (row) - { - let attribute = row[0]; - if (!pUseAttributeIds) - { - if (!(attribute in attributeNameMap)) - attributeNameMap[attribute] = AttributeUtil.getFullAttributeName(attribute); - attribute = attributeNameMap[attribute]; - } - let value = row[AttributeTypeUtil.getTypeColumnIndex(row[1]) + defaultFields.length]; - if (!pUseIdValues && row[1].trim() == $AttributeTypes.COMBO) - value = row[3]; - else if (!pUseIdValues) - value = AttributeTypeUtil.getAttributeViewValue(row[1].trim(), value, row[2]); - - return [attribute, value]; - }); - - return attributeValues; -} - -/** - * 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 sqlSelect = "select ATTRIBUTE_TYPE, KEYWORD_CONTAINER from AB_ATTRIBUTE"; - var type = db.array(db.ROW, SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) - .buildSql(sqlSelect) - ); - if (!type.length) - return null; - - type[0] = type[0].trim(); - var field = AttributeTypeUtil.getDatabaseField(type[0]); - var value = pValueMap[field]; - if (pGetViewValue && type[0] == $AttributeTypes.COMBO) - { - value = db.cell(SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", value) - .buildSql("select ATTRIBUTE_NAME from AB_ATTRIBUTE") - ); - } - else if (pGetViewValue) - value = AttributeTypeUtil.getAttributeViewValue(type[0], value, type[1]); - - return value; -} - -AttributeRelationUtils.getAttributes = function () -{ - //TODO: implement maybe -} - -/** - * sets the value of an attribute for one dataset (e. g. a person) - */ -AttributeRelationUtils.setAttribute = function () -{ - //TODO: implement -} - -/** - * adds rows for attributes with min_count > 0 - * - * @param {String} pObjectType the object type - * @param {String} pConsumer the name of the attribute relation consumer - */ -AttributeRelationUtils.presetMandatoryAttributes = function (pObjectType, pConsumer) -{ - var mandatoryAttributes = db.table( - SqlCondition.begin() - .andPrepare("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType) - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# <> ?") - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.GROUP, "# <> ?") - .and("ATTRIBUTE_ACTIVE = 1") - .and("MIN_COUNT > 0") - .buildSql("select AB_ATTRIBUTE_ID, MIN_COUNT from AB_ATTRIBUTEUSAGE " - + "join AB_ATTRIBUTE on AB_ATTRIBUTE_ID = AB_ATTRIBUTEID") - ); - 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] - }); - }); -} - -/** - * 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 - * - * @return {String} the validation message or an empty string if everything is ok - */ -AttributeRelationUtils.validateAttributeCount = function (pRowId, pObjectType, pConsumerField) -{ - 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 - 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 attributeCondition = SqlCondition.begin(); - AttributeUtil.getPossibleAttributes(pObjectType).forEach(function (attributeId) - { - this.orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId); - }, attributeCondition); - - var usageCondition = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType) - .andSqlCondition(attributeCondition, "1=2"); - - //retrieve all min/max counts of the possible attributes - var minMaxCounts = db.table(usageCondition.buildSql( - "select AB_ATTRIBUTEID, ATTRIBUTE_NAME, MIN_COUNT, MAX_COUNT from AB_ATTRIBUTEUSAGE " - + "join AB_ATTRIBUTE on AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID", "1=2" - )); - - 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 chossing 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) -{ - var countObj = {}; - var condition = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTERELATION.OBJECT_ROWID", pRowId); - if (pObjectType) - condition.andPrepare("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType); - var attributeRelations = db.table(condition.buildSql( - "select AB_ATTRIBUTERELATIONID, AB_ATTRIBUTE_ID from AB_ATTRIBUTERELATION", - "1=2" - )); - attributeRelations.forEach(function ([relationId, attributeId]) - { - if (pAttributeChanges && relationId in pAttributeChanges) - attributeId = pAttributeChanges[relationId]; - if (attributeId !== "") - this[attributeId] = (this[attributeId] || 0) + 1; - }, countObj); - - 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 values for each type are: - * - * keyword = the key of the corresponding keyword - * 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 - * - * The display name is controlled by the keyword 'AttributeType' - */ -function $AttributeTypes () {} - -$AttributeTypes.TEXT = { - toString : function () {return this.keyword}, - keyword : "TEXT", - contentType : "TEXT", - databaseField : "CHAR_VALUE" -}; -$AttributeTypes.DATE = { - toString : function () {return this.keyword}, - keyword : "DATE", - contentType : "DATE", - databaseField : "DATE_VALUE", - getViewValue : function (pValue) - { - return datetime.toDate(pValue, translate.text("dd.MM.yyyy")); - } -}; -$AttributeTypes.NUMBER = { - toString : function () {return this.keyword}, - keyword : "NUMBER", - contentType : "NUMBER", - databaseField : "NUMBER_VALUE" -}; -$AttributeTypes.BOOLEAN = { - toString : function () {return this.keyword}, - keyword : "BOOLEAN", - contentType : "BOOLEAN", - databaseField : "INT_VALUE", - getViewValue : function (pValue) - { - return pValue == "1" ? translate.text("Yes") : translate.text("No"); - } -}; -$AttributeTypes.COMBO = { - toString : function () {return this.keyword}, - keyword : "COMBO", - contentType : "UNKNOWN", - databaseField : "ID_VALUE", - isGroup : true -}; -$AttributeTypes.COMBOVALUE = { - toString : function () {return this.keyword}, - keyword : "COMBOVALUE", - contentType : null, - databaseField : null -}; -$AttributeTypes.GROUP = { - toString : function () {return this.keyword}, - keyword : "GROUP", - contentType : null, - databaseField : null, - isGroup : true -}; -$AttributeTypes.KEYWORD = { - toString : function () {return this.keyword}, - keyword : "KEYWORD", - contentType : "UNKNOWN", - databaseField : "ID_VALUE", - getViewValue : function (pValue, pKeyword) - { - return KeywordUtils.getViewValue(pKeyword, pValue); - } -}; -$AttributeTypes.VOID = { - toString : function () {return this.keyword}, - keyword : "VOID", - contentType : null, - databaseField : null, - isGroup : true -}; -$AttributeTypes.MEMO = { - toString : function () {return this.keyword}, - keyword : "MEMO", - contentType : "LONG_TEXT", - databaseField : "CHAR_VALUE" -}; - -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) -{ - if (pAttributeType in $AttributeTypes) - return $AttributeTypes[pAttributeType].contentType; - return null; -} - -/** - * 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) -{ - if (pAttributeType in $AttributeTypes) - return $AttributeTypes[pAttributeType].isGroup || false; - return null; -} - -/** - * 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) -{ - if (pAttributeType in $AttributeTypes) - return $AttributeTypes[pAttributeType].databaseField; - return null; -} - -AttributeTypeUtil.getAttributeViewValue = function (pAttributeType, pValue, pKeyword) -{ - if (pAttributeType in $AttributeTypes && "getViewValue" in $AttributeTypes[pAttributeType]) - 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! - * - * @class - */ -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) -{ - var table = "AB_ATTRIBUTEUSAGE"; - var columns = ["AB_ATTRIBUTEUSAGEID", "AB_ATTRIBUTE_ID", "OBJECT_TYPE"]; - var types = db.getColumnTypes(table, columns); - - var sqlSelect = "select AB_ATTRIBUTEID, " - + " exists (select AB_ATTRIBUTEUSAGEID from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID and OBJECT_TYPE = '" - + pObjectType + "') from AB_ATTRIBUTE"; - - var inserts = []; - _addInserts(pAttributeId, pObjectType); - db.inserts(inserts); - - function _addInserts (pAttributeId, pObjectType) - { - var condition = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# != ?") - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId); - var attributes = db.table(condition.buildSql(sqlSelect)); - - attributes.forEach(function (row) - { - if (row[1] != "true") - { - let values = [util.getNewUUID(), row[0], pObjectType]; - 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) - return; - - var table = "AB_ATTRIBUTEUSAGE"; - - var sqlSelect = "select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID, " - + " exists (select AB_ATTRIBUTEUSAGEID from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID and OBJECT_TYPE = '" - + pNewObjectType + "')" - + " from AB_ATTRIBUTE left join AB_ATTRIBUTEUSAGE on AB_ATTRIBUTEID = AB_ATTRIBUTE_ID and OBJECT_TYPE = '" + pOldObjectType + "'"; - - var updateCond = SqlCondition.begin(); - - //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 = SqlCondition.begin(); - - _addUpdateIds(pAttributeId, pOldObjectType); - - if (updateCond.isSet()) - db.updateData(table, ["OBJECT_TYPE"], null, [pNewObjectType], updateCond.build("1=2")); - if (deleteCond.isSet()) - db.deleteData(table, deleteCond.build("1=2")); - - function _addUpdateIds (pAttributeId) - { - var condition = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# != ?") - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId); - var attributes = db.table(condition.buildSql(sqlSelect)); - - attributes.forEach(function (row) - { - if (row[1] && row[2] == "true") - deleteCond.orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1]); - else if (row[1]) - updateCond.orPrepare("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 table = "AB_ATTRIBUTEUSAGE"; - - var sqlSelect = "select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID " - + " from AB_ATTRIBUTE left join AB_ATTRIBUTEUSAGE on AB_ATTRIBUTEID = AB_ATTRIBUTE_ID and OBJECT_TYPE = '" + pObjectType + "'"; - - var deleteCond = SqlCondition.begin(); - _addDeleteIds(pAttributeId, pObjectType); - if (deleteCond.isSet()) - db.deleteData(table, deleteCond.build("1=2")); - - function _addDeleteIds (pAttributeId) - { - var condition = SqlCondition.begin() - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# != ?") - .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId); - var attributes = db.table(condition.buildSql(sqlSelect)); - - attributes.forEach(function (row) - { - if (row[1]) - deleteCond.orPrepare("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 condition = SqlCondition.begin() - .and("exists (select AB_ATTRIBUTEUSAGEID from AB_ATTRIBUTEUSAGE AU where AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AU.AB_ATTRIBUTE_ID " - + "and AB_ATTRIBUTEUSAGE.OBJECT_TYPE = AU.OBJECT_TYPE and AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID != AU.AB_ATTRIBUTEUSAGEID)"); - if (pAttributeId) - condition.andPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", pAttributeId); - - var duplicates = db.table(condition.buildSql("select AB_ATTRIBUTEUSAGEID, AB_ATTRIBUTE_ID, OBJECT_TYPE from AB_ATTRIBUTEUSAGE")); - var usageObj = {}; - var deleteCond = SqlCondition.begin(); - - duplicates.forEach(function (row) - { - if (!(row[1] in this)) - this[row[1]] = {}; - if (row[2] in this[row[1]]) - deleteCond.orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[0]); - this[row[1]][row[2]] = true; - }, usageObj); - if (deleteCond.isSet()) - db.deleteData("AB_ATTRIBUTEUSAGE", deleteCond.build("1=2")); +import("system.logging"); +import("system.util"); +import("system.datetime"); +import("system.translate"); +import("system.neon"); +import("system.vars"); +import("system.db"); +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 () {} + +/** + * 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 attrSql = "select AB_ATTRIBUTEID from AB_ATTRIBUTE" + + " join AB_ATTRIBUTEUSAGE on AB_ATTRIBUTEID = AB_ATTRIBUTE_ID"; + var attrCond = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType) + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# <> ?") + .and("ATTRIBUTE_ACTIVE = 1"); + + if (pAttributeCount) + { + for (let attributeId in pAttributeCount) + { + attrCond.andSqlCondition( + SqlCondition.begin() + .orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId, "# != ?") + .orPrepare("AB_ATTRIBUTEUSAGE.MAX_COUNT", pAttributeCount[attributeId], "# > ?") + .or("AB_ATTRIBUTEUSAGE.MAX_COUNT is null") + ); + } + } + + if (pFilteredAttributeIds) + { + var filteredIdsCondition = new SqlCondition(); + var filteredIdChildren = AttributeUtil.getAllChildren(pFilteredAttributeIds); + pFilteredAttributeIds.concat(filteredIdChildren).forEach(function(id) + { + this.orPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", id); + }, filteredIdsCondition); + + attrCond.andSqlCondition(filteredIdsCondition); + } + + if (!pIncludeGroups) + attrCond.and("ATTRIBUTE_TYPE != '" + $AttributeTypes.GROUP + "'"); + + var attributes = db.array(db.COLUMN, attrCond.buildSql(attrSql)); + + return attributes; +} + +/** + * 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. + * + * @return {String} the name of the attribute + */ +AttributeUtil.getFullAttributeName = function (pAttributeId, pSimpleName) +{ + if (pSimpleName == undefined) + pSimpleName = false; + + if (!pAttributeId) + return ""; + if (pSimpleName) + return AttributeUtil.getSimpleAttributeName(pAttributeId); + var attributeNames = []; + var attribute; + do { + attribute = db.array(db.ROW, SqlCondition.begin() + .andPrepare(["AB_ATTRIBUTE", "AB_ATTRIBUTEID", "ATTRIBUTE"], pAttributeId) + .buildSql("select ATTRIBUTE.ATTRIBUTE_NAME, PARENT1.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_NAME, PARENT2.ATTRIBUTE_PARENT_ID \n\ + from AB_ATTRIBUTE ATTRIBUTE \n\ + left join AB_ATTRIBUTE PARENT1 on ATTRIBUTE.ATTRIBUTE_PARENT_ID = PARENT1.AB_ATTRIBUTEID \n\ + left join AB_ATTRIBUTE PARENT2 on PARENT1.ATTRIBUTE_PARENT_ID = PARENT2.AB_ATTRIBUTEID") + ); + if (attribute.length > 0) + { + attributeNames.push(attribute[0]); + if (attribute[1]) + attributeNames.push(attribute[1]); + if (attribute[2]) + attributeNames.push(attribute[2]); + pAttributeId = attribute[3]; + } + else + pAttributeId = ""; + } while (pAttributeId); + + return attributeNames.reverse().join(" / "); +} + +/** + * returns the name of an attribute + * + * @param {String} pAttributeId the id of the attribute + * + * @return {String} the name of the attribute + */ +AttributeUtil.getSimpleAttributeName = function (pAttributeId) +{ + var attributeName = db.cell(SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) + .buildSql("select ATTRIBUTE_NAME from AB_ATTRIBUTE") + ); + + 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 = db.array(db.COLUMN, SqlCondition.begin() + .and("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID in ('" + pAttributeIds.join("','") + "')") + .buildSql("select AB_ATTRIBUTEID from AB_ATTRIBUTE") + ); + 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 db.cell(SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) + .buildSql( + "select exists (" + + "select AB_ATTRIBUTERELATIONID from AB_ATTRIBUTERELATION " + + "where AB_ATTRIBUTE_ID = AB_ATTRIBUTEID" + + ") from AB_ATTRIBUTE", "1=2" + ) + ) == "true"; +} + +/** + * returns the type of an attribute + * + * @param {String} pAttributeId the id of the attribute + * + * @result {String} attribute type + */ +AttributeUtil.getAttributeType = function (pAttributeId) +{ + var attrTypeSelect = "select ATTRIBUTE_TYPE from AB_ATTRIBUTE"; + attrTypeSelect = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) + .buildSql(attrTypeSelect); + return db.cell(attrTypeSelect).trim(); +} + +/*********************************************************************************************************************/ + +/** + * Provides functions for the work with attributeRelations, getting the value of an attributeRelation for an object. + * Don't instanciate this! + * + * @class + */ +function AttributeRelationUtils () {} + +/** + * 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 + * + * @return {String|null} the value of the attribute + */ +AttributeRelationUtils.getAttribute = function (pAttributeId, pObjectRowId, pObjectType, pGetViewValue) +{ + var attrCond = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTERELATION.OBJECT_ROWID", pObjectRowId) + .andPrepare("AB_ATTRIBUTERELATION.AB_ATTRIBUTE_ID", pAttributeId); + if (pObjectType != null) + attrCond.andPrepare("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType); + + var defaultFields = [ + "AB_ATTRIBUTE.ATTRIBUTE_TYPE", + "AB_ATTRIBUTE.KEYWORD_CONTAINER", + "COMBOVAL.ATTRIBUTE_NAME" + ]; + var valueFields = AttributeTypeUtil.getAllDatabaseFields(); + var attributeSql = attrCond.buildSql("select " + defaultFields.join(", ") + ", " + valueFields.join(", ") + + " from AB_ATTRIBUTERELATION join AB_ATTRIBUTE on AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID" + + " left join AB_ATTRIBUTE COMBOVAL on " + $AttributeTypes.COMBO.databaseField + " = COMBOVAL.AB_ATTRIBUTEID"); + + var attributeValues = db.array(db.ROW, attributeSql); + if (!attributeValues.length) + return null; + + let value = attributeValues[AttributeTypeUtil.getTypeColumnIndex(attributeValues[0]) + defaultFields.length]; + if (pGetViewValue && attributeValues[1].trim() == $AttributeTypes.COMBO) + value = attributeValues[2]; + else if (pGetViewValue) + value = AttributeTypeUtil.getAttributeViewValue(attributeValues[0].trim(), value, attributeValues[1]); + + return value; +} + +/** + * gets all attributes for a dataset + * + * @param {String} pObjectRowId object rowid + * @param {String} [pObjectType=null] object-type + * @param {String} [pUseAttributeIds=false] if true the ids are used instead of the full attribute names + * @param {String} [pUseIdValues=false] if true the values are not resolved or formatted + * + * @return {String[][]} two-dimensional array a row is [attributeId|attributeName, value] + */ +AttributeRelationUtils.getAllAttributes = function (pObjectRowId, pObjectType, pUseAttributeIds, pUseIdValues) +{ + var attrCond = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTERELATION.OBJECT_ROWID", pObjectRowId); + if (pObjectType != null) + attrCond.andPrepare("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType); + + var defaultFields = [ + "AB_ATTRIBUTE_ID", + "AB_ATTRIBUTE.ATTRIBUTE_TYPE", + "AB_ATTRIBUTE.KEYWORD_CONTAINER", + "COMBOVAL.ATTRIBUTE_NAME" + ]; + var valueFields = AttributeTypeUtil.getAllDatabaseFields(); + var attributeSql = attrCond.buildSql("select " + defaultFields.join(", ") + ", " + valueFields.join(", ") + + " from AB_ATTRIBUTERELATION join AB_ATTRIBUTE on AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID" + + " left join AB_ATTRIBUTE COMBOVAL on " + $AttributeTypes.COMBO.databaseField + " = COMBOVAL.AB_ATTRIBUTEID"); + + var attributeNameMap = {}; + var attributeValues = db.table(attributeSql).map(function (row) + { + let attribute = row[0]; + if (!pUseAttributeIds) + { + if (!(attribute in attributeNameMap)) + attributeNameMap[attribute] = AttributeUtil.getFullAttributeName(attribute); + attribute = attributeNameMap[attribute]; + } + let value = row[AttributeTypeUtil.getTypeColumnIndex(row[1]) + defaultFields.length]; + if (!pUseIdValues && row[1].trim() == $AttributeTypes.COMBO) + value = row[3]; + else if (!pUseIdValues) + value = AttributeTypeUtil.getAttributeViewValue(row[1].trim(), value, row[2]); + + return [attribute, value]; + }); + + return attributeValues; +} + +/** + * 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 sqlSelect = "select ATTRIBUTE_TYPE, KEYWORD_CONTAINER from AB_ATTRIBUTE"; + var type = db.array(db.ROW, SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", pAttributeId) + .buildSql(sqlSelect) + ); + if (!type.length) + return null; + + type[0] = type[0].trim(); + var field = AttributeTypeUtil.getDatabaseField(type[0]); + var value = pValueMap[field]; + if (pGetViewValue && type[0] == $AttributeTypes.COMBO) + { + value = db.cell(SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.AB_ATTRIBUTEID", value) + .buildSql("select ATTRIBUTE_NAME from AB_ATTRIBUTE") + ); + } + else if (pGetViewValue) + value = AttributeTypeUtil.getAttributeViewValue(type[0], value, type[1]); + + return value; +} + +AttributeRelationUtils.getAttributes = function () +{ + //TODO: implement maybe +} + +/** + * sets the value of an attribute for one dataset (e. g. a person) + */ +AttributeRelationUtils.setAttribute = function () +{ + //TODO: implement +} + +/** + * adds rows for attributes with min_count > 0 + * + * @param {String} pObjectType the object type + * @param {String} pConsumer the name of the attribute relation consumer + */ +AttributeRelationUtils.presetMandatoryAttributes = function (pObjectType, pConsumer) +{ + var mandatoryAttributes = db.table( + SqlCondition.begin() + .andPrepare("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType) + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# <> ?") + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.GROUP, "# <> ?") + .and("ATTRIBUTE_ACTIVE = 1") + .and("MIN_COUNT > 0") + .buildSql("select AB_ATTRIBUTE_ID, MIN_COUNT from AB_ATTRIBUTEUSAGE " + + "join AB_ATTRIBUTE on AB_ATTRIBUTE_ID = AB_ATTRIBUTEID") + ); + 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] + }); + }); +} + +/** + * 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 + * + * @return {String} the validation message or an empty string if everything is ok + */ +AttributeRelationUtils.validateAttributeCount = function (pRowId, pObjectType, pConsumerField) +{ + var attributeChanges = {}; + var deletedRows = vars.get("$field." + pConsumerField + ".deletedRows"); + var changedRows = vars.get("$field." + pConsumerField + ".changedRows"); + var insertedRows = vars.get("$field." + pConsumerField + ".insertedRows"); + logging.log("------------------------"); + logging.log("insertedRows = " + JSON.stringify(insertedRows, null, " ")); + logging.log("changedRows = " + JSON.stringify(changedRows, null, " ")); + logging.log("deletedRows = " + JSON.stringify(deletedRows, null, " ")); + + + 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); + logging.log("merged ones (countObj init):" + JSON.stringify(countObj, null, " ")); + + if (insertedRows) //append the new rows + { + insertedRows.forEach(function (row) + { + this[row.AB_ATTRIBUTE_ID] = (this[row.AB_ATTRIBUTE_ID] || 0) + 1; + }, countObj); + } + var attributeCondition = SqlCondition.begin(); + AttributeUtil.getPossibleAttributes(pObjectType).forEach(function (attributeId) + { + this.orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", attributeId); + }, attributeCondition); + + var usageCondition = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTEUSAGE.OBJECT_TYPE", pObjectType) + .andSqlCondition(attributeCondition, "1=2"); + + //retrieve all min/max counts of the possible attributes + var minMaxCounts = db.table(usageCondition.buildSql( + "select AB_ATTRIBUTEID, ATTRIBUTE_NAME, MIN_COUNT, MAX_COUNT from AB_ATTRIBUTEUSAGE " + + "join AB_ATTRIBUTE on AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID", "1=2" + )); + + 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 chossing 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 = {}; + + var condition = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTERELATION.OBJECT_ROWID", pRowId) + .andPrepareIfSet("AB_ATTRIBUTERELATION.OBJECT_TYPE", pObjectType); + + var storedAttributeRelations = db.table(condition.buildSql( + "select AB_ATTRIBUTERELATIONID, AB_ATTRIBUTE_ID from AB_ATTRIBUTERELATION", + "1=2" + )); + + storedAttributeRelations.forEach(function ([storedAttrRelationId, storedAttributeId]) { + 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 values for each type are: + * + * keyword = the key of the corresponding keyword + * 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 + * + * The display name is controlled by the keyword 'AttributeType' + */ +function $AttributeTypes () {} + +$AttributeTypes.TEXT = { + toString : function () {return this.keyword}, + keyword : "TEXT", + contentType : "TEXT", + databaseField : "CHAR_VALUE" +}; +$AttributeTypes.DATE = { + toString : function () {return this.keyword}, + keyword : "DATE", + contentType : "DATE", + databaseField : "DATE_VALUE", + getViewValue : function (pValue) + { + return datetime.toDate(pValue, translate.text("dd.MM.yyyy")); + } +}; +$AttributeTypes.NUMBER = { + toString : function () {return this.keyword}, + keyword : "NUMBER", + contentType : "NUMBER", + databaseField : "NUMBER_VALUE" +}; +$AttributeTypes.BOOLEAN = { + toString : function () {return this.keyword}, + keyword : "BOOLEAN", + contentType : "BOOLEAN", + databaseField : "INT_VALUE", + getViewValue : function (pValue) + { + return pValue == "1" ? translate.text("Yes") : translate.text("No"); + } +}; +$AttributeTypes.COMBO = { + toString : function () {return this.keyword}, + keyword : "COMBO", + contentType : "UNKNOWN", + databaseField : "ID_VALUE", + isGroup : true +}; +$AttributeTypes.COMBOVALUE = { + toString : function () {return this.keyword}, + keyword : "COMBOVALUE", + contentType : null, + databaseField : null +}; +$AttributeTypes.GROUP = { + toString : function () {return this.keyword}, + keyword : "GROUP", + contentType : null, + databaseField : null, + isGroup : true +}; +$AttributeTypes.KEYWORD = { + toString : function () {return this.keyword}, + keyword : "KEYWORD", + contentType : "UNKNOWN", + databaseField : "ID_VALUE", + getViewValue : function (pValue, pKeyword) + { + return KeywordUtils.getViewValue(pKeyword, pValue); + } +}; +$AttributeTypes.VOID = { + toString : function () {return this.keyword}, + keyword : "VOID", + contentType : null, + databaseField : null, + isGroup : true +}; +$AttributeTypes.MEMO = { + toString : function () {return this.keyword}, + keyword : "MEMO", + contentType : "LONG_TEXT", + databaseField : "CHAR_VALUE" +}; + +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) +{ + if (pAttributeType in $AttributeTypes) + return $AttributeTypes[pAttributeType].contentType; + return null; +} + +/** + * 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) +{ + if (pAttributeType in $AttributeTypes) + return $AttributeTypes[pAttributeType].isGroup || false; + return null; +} + +/** + * 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) +{ + if (pAttributeType in $AttributeTypes) + return $AttributeTypes[pAttributeType].databaseField; + return null; +} + +AttributeTypeUtil.getAttributeViewValue = function (pAttributeType, pValue, pKeyword) +{ + if (pAttributeType in $AttributeTypes && "getViewValue" in $AttributeTypes[pAttributeType]) + 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! + * + * @class + */ +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) +{ + var table = "AB_ATTRIBUTEUSAGE"; + var columns = ["AB_ATTRIBUTEUSAGEID", "AB_ATTRIBUTE_ID", "OBJECT_TYPE"]; + var types = db.getColumnTypes(table, columns); + + var sqlSelect = "select AB_ATTRIBUTEID, " + + " exists (select AB_ATTRIBUTEUSAGEID from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID and OBJECT_TYPE = '" + + pObjectType + "') from AB_ATTRIBUTE"; + + var inserts = []; + _addInserts(pAttributeId, pObjectType); + db.inserts(inserts); + + function _addInserts (pAttributeId, pObjectType) + { + var condition = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# != ?") + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId); + var attributes = db.table(condition.buildSql(sqlSelect)); + + attributes.forEach(function (row) + { + if (row[1] != "true") + { + let values = [util.getNewUUID(), row[0], pObjectType]; + 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) + return; + + var table = "AB_ATTRIBUTEUSAGE"; + + var sqlSelect = "select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID, " + + " exists (select AB_ATTRIBUTEUSAGEID from AB_ATTRIBUTEUSAGE where AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AB_ATTRIBUTE.AB_ATTRIBUTEID and OBJECT_TYPE = '" + + pNewObjectType + "')" + + " from AB_ATTRIBUTE left join AB_ATTRIBUTEUSAGE on AB_ATTRIBUTEID = AB_ATTRIBUTE_ID and OBJECT_TYPE = '" + pOldObjectType + "'"; + + var updateCond = SqlCondition.begin(); + + //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 = SqlCondition.begin(); + + _addUpdateIds(pAttributeId, pOldObjectType); + + if (updateCond.isSet()) + db.updateData(table, ["OBJECT_TYPE"], null, [pNewObjectType], updateCond.build("1=2")); + if (deleteCond.isSet()) + db.deleteData(table, deleteCond.build("1=2")); + + function _addUpdateIds (pAttributeId) + { + var condition = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# != ?") + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId); + var attributes = db.table(condition.buildSql(sqlSelect)); + + attributes.forEach(function (row) + { + if (row[1] && row[2] == "true") + deleteCond.orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[1]); + else if (row[1]) + updateCond.orPrepare("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 table = "AB_ATTRIBUTEUSAGE"; + + var sqlSelect = "select AB_ATTRIBUTEID, AB_ATTRIBUTEUSAGEID " + + " from AB_ATTRIBUTE left join AB_ATTRIBUTEUSAGE on AB_ATTRIBUTEID = AB_ATTRIBUTE_ID and OBJECT_TYPE = '" + pObjectType + "'"; + + var deleteCond = SqlCondition.begin(); + _addDeleteIds(pAttributeId, pObjectType); + if (deleteCond.isSet()) + db.deleteData(table, deleteCond.build("1=2")); + + function _addDeleteIds (pAttributeId) + { + var condition = SqlCondition.begin() + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_TYPE", $AttributeTypes.COMBOVALUE, "# != ?") + .andPrepare("AB_ATTRIBUTE.ATTRIBUTE_PARENT_ID", pAttributeId); + var attributes = db.table(condition.buildSql(sqlSelect)); + + attributes.forEach(function (row) + { + if (row[1]) + deleteCond.orPrepare("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 condition = SqlCondition.begin() + .and("exists (select AB_ATTRIBUTEUSAGEID from AB_ATTRIBUTEUSAGE AU where AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID = AU.AB_ATTRIBUTE_ID " + + "and AB_ATTRIBUTEUSAGE.OBJECT_TYPE = AU.OBJECT_TYPE and AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID != AU.AB_ATTRIBUTEUSAGEID)"); + if (pAttributeId) + condition.andPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTE_ID", pAttributeId); + + var duplicates = db.table(condition.buildSql("select AB_ATTRIBUTEUSAGEID, AB_ATTRIBUTE_ID, OBJECT_TYPE from AB_ATTRIBUTEUSAGE")); + var usageObj = {}; + var deleteCond = SqlCondition.begin(); + + duplicates.forEach(function (row) + { + if (!(row[1] in this)) + this[row[1]] = {}; + if (row[2] in this[row[1]]) + deleteCond.orPrepare("AB_ATTRIBUTEUSAGE.AB_ATTRIBUTEUSAGEID", row[0]); + this[row[1]][row[2]] = true; + }, usageObj); + if (deleteCond.isSet()) + db.deleteData("AB_ATTRIBUTEUSAGE", deleteCond.build("1=2")); } \ No newline at end of file