From aa26ca8dac82986714d0916d593ca1bfee896063 Mon Sep 17 00:00:00 2001 From: Ken Zangelin Date: Thu, 18 Jan 2024 19:31:54 +0100 Subject: [PATCH] First draft for VocabProperty --- CHANGES_NEXT_RELEASE | 6 + .../dbModel/dbModelFromApiAttribute.cpp | 6 +- .../orionld/dbModel/dbModelFromApiEntity.cpp | 2 +- .../dbModel/dbModelFromApiSubAttribute.cpp | 5 +- .../orionld/dbModel/dbModelToApiAttribute.cpp | 36 +- .../dbModel/dbModelToApiSubAttribute.cpp | 10 +- .../notifications/notificationSend.cpp | 21 +- .../orionld/payloadCheck/pCheckAttribute.cpp | 174 ++++++++-- .../orionld/types/OrionldAttributeType.cpp | 20 +- src/lib/orionld/types/OrionldAttributeType.h | 3 +- ...sild_new_entity_create-error-handling.test | 8 +- ...gsild_simplified_POST_Entities_ERRORS.test | 4 +- .../0000_ngsild/ngsild_vocab-property.test | 320 ++++++++++++++++++ 13 files changed, 555 insertions(+), 60 deletions(-) create mode 100644 test/functionalTest/cases/0000_ngsild/ngsild_vocab-property.test diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index e69de29bb2..6e1bb69777 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -0,0 +1,6 @@ +Fixed Issue: + * #1535 No matching with 'q' when matching subscriptions for deletion of an entity + +New Features: + * Support for VocabularyProperty (highlu untested and 'q' expansions for vocab-properties is not yet implemented) + diff --git a/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp b/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp index cd762cf909..3a85705082 100644 --- a/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp +++ b/src/lib/orionld/dbModel/dbModelFromApiAttribute.cpp @@ -56,7 +56,7 @@ extern "C" // * First an object "md" is created, and all fields of the attribute, except the special ones are moved inside "md". // Special fields: // - type -// - value/object/languageMap (must be renamed to "value" - that's part of the database model) +// - value/object/languageMap/vocab (must be called "value" - that's part of the database model) // - observedAt // - datasetId // - unitCode @@ -149,13 +149,13 @@ bool dbModelFromApiAttribute(KjNode* attrP, KjNode* dbAttrsP, KjNode* attrAddedV // // Move special fields back to "attrP" - const char* specialV[] = { "type", "value", "object", "languageMap", "datasetId" }; // observedAt+unitCode are mds (db-model) + const char* specialV[] = { "type", "value", "object", "languageMap", "vocab", "datasetId" }; // observedAt+unitCode are mds (db-model) for (unsigned int ix = 0; ix < K_VEC_SIZE(specialV); ix++) { KjNode* nodeP = kjLookup(mdP, specialV[ix]); if (nodeP != NULL) { - if ((ix == 2) || (ix == 3)) // "object", "languageMap", change name to "value" (Orion's DB model) + if ((ix == 2) || (ix == 3) || (ix == 4)) // "object", "languageMap", "vocab": change name to "value" (Orion's DB model) nodeP->name = (char*) "value"; kjChildRemove(mdP, nodeP); diff --git a/src/lib/orionld/dbModel/dbModelFromApiEntity.cpp b/src/lib/orionld/dbModel/dbModelFromApiEntity.cpp index 659c25d3eb..c7a8d9998b 100644 --- a/src/lib/orionld/dbModel/dbModelFromApiEntity.cpp +++ b/src/lib/orionld/dbModel/dbModelFromApiEntity.cpp @@ -66,7 +66,7 @@ extern "C" // * If Array, recursive call for each member (set the name to the sttribute name) // * "datasetId" present - call orionldDbModelAttributeDatasetId // * "type" can't be modified -// * "value"/"object"/"languageMap" changes name to "value" and RHS stays as is +// * "value"/"object"/"languageMap"/"vocab" changes name to "value" and RHS stays as is // * "observedAt" is made an Object with single member "value" // * "unitCode" is made an Object with single member "value" // * "md" is created and added diff --git a/src/lib/orionld/dbModel/dbModelFromApiSubAttribute.cpp b/src/lib/orionld/dbModel/dbModelFromApiSubAttribute.cpp index a5cd30a37a..562dad857b 100644 --- a/src/lib/orionld/dbModel/dbModelFromApiSubAttribute.cpp +++ b/src/lib/orionld/dbModel/dbModelFromApiSubAttribute.cpp @@ -77,7 +77,7 @@ bool dbModelFromApiSubAttribute(KjNode* saP, KjNode* dbMdP, KjNode* mdAddedV, Kj if (strcmp(saDotName, "value") == 0) return true; - else if ((strcmp(saDotName, "object") == 0) || (strcmp(saDotName, "languageMap") == 0)) + else if ((strcmp(saDotName, "object") == 0) || (strcmp(saDotName, "languageMap") == 0) || (strcmp(saDotName, "vocab") == 0)) saDotName = (char*) "value"; // Orion-LD's database model states that all attributes have a "value" else if (strcmp(saDotName, "observedAt") == 0) { @@ -155,6 +155,9 @@ bool dbModelFromApiSubAttribute(KjNode* saP, KjNode* dbMdP, KjNode* mdAddedV, Kj if (valueP == NULL) valueP = kjLookup(saP, "languageMap"); + if (valueP == NULL) + valueP = kjLookup(saP, "vocab"); + if (valueP != NULL) valueP->name = (char*) "value"; diff --git a/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp b/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp index 4458b0ffbb..1066a2e7c6 100644 --- a/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp +++ b/src/lib/orionld/dbModel/dbModelToApiAttribute.cpp @@ -111,8 +111,23 @@ void dbModelToApiAttribute(KjNode* dbAttrP, bool sysAttrs, bool eqsForDots) if ((typeP != NULL) && (valueP != NULL) && (typeP->type == KjString)) { - if (strcmp(typeP->value.s, "Relationship") == 0) valueP->name = (char*) "object"; - else if (strcmp(typeP->value.s, "LanguageProperty") == 0) valueP->name = (char*) "languageMap"; + if (strcmp(typeP->value.s, "Relationship") == 0) valueP->name = (char*) "object"; + else if (strcmp(typeP->value.s, "LanguageProperty") == 0) valueP->name = (char*) "languageMap"; + else if (strcmp(typeP->value.s, "VocabularyProperty") == 0) + { + valueP->name = (char*) "vocab"; + + if (valueP->type == KjString) + valueP->value.s = orionldContextItemAliasLookup(orionldState.contextP, valueP->value.s, NULL, NULL); + else if (valueP->type == KjArray) + { + for (KjNode* wordP = valueP->value.firstChildP; wordP != NULL; wordP = wordP->next) + { + if (wordP->type == KjString) + wordP->value.s = orionldContextItemAliasLookup(orionldState.contextP, wordP->value.s, NULL, NULL); + } + } + } } @@ -334,7 +349,7 @@ KjNode* dbModelToApiAttribute2(KjNode* dbAttrP, KjNode* datasetP, bool sysAttrs, // - and No metadata // => KeyValues // - // Else, just remove the attribute type, keep value/object/languageMap + // Else, just remove the attribute type, keep value/object/languageMap/vocab // And call dbModelToApiSubAttribute2 with Concise // bool conciseAsKeyValues = false; @@ -398,6 +413,21 @@ KjNode* dbModelToApiAttribute2(KjNode* dbAttrP, KjNode* datasetP, bool sysAttrs, { if (attrType == Relationship) nodeP->name = (char*) "object"; + else if (attrType == VocabularyProperty) + { + nodeP->name = (char*) "vocab"; + + if (nodeP->type == KjString) + nodeP->value.s = orionldContextItemAliasLookup(orionldState.contextP, nodeP->value.s, NULL, NULL); + else if (nodeP->type == KjArray) + { + for (KjNode* wordP = nodeP->value.firstChildP; wordP != NULL; wordP = wordP->next) + { + if (wordP->type == KjString) + wordP->value.s = orionldContextItemAliasLookup(orionldState.contextP, wordP->value.s, NULL, NULL); + } + } + } else if (attrType == LanguageProperty) { if (lang != NULL) diff --git a/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp b/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp index 011d69d365..034b9018a1 100644 --- a/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp +++ b/src/lib/orionld/dbModel/dbModelToApiSubAttribute.cpp @@ -82,8 +82,9 @@ void dbModelToApiSubAttribute(KjNode* dbSubAttrP) if ((typeP != NULL) && (valueP != NULL) && (typeP->type == KjString)) { - if (strcmp(typeP->value.s, "Relationship") == 0) valueP->name = (char*) "object"; - else if (strcmp(typeP->value.s, "LanguageProperty") == 0) valueP->name = (char*) "languageMap"; + if (strcmp(typeP->value.s, "Relationship") == 0) valueP->name = (char*) "object"; + else if (strcmp(typeP->value.s, "LanguageProperty") == 0) valueP->name = (char*) "languageMap"; + else if (strcmp(typeP->value.s, "VocabularyProperty") == 0) valueP->name = (char*) "vocab"; } } @@ -140,8 +141,9 @@ KjNode* dbModelToApiSubAttribute2(KjNode* dbSubAttributeP, bool sysAttrs, Orionl if (strcmp(nodeP->name, "value") == 0) { - if (subAttrType == Relationship) nodeP->name = (char*) "object"; - else if (subAttrType == LanguageProperty) nodeP->name = (char*) "languageMap"; + if (subAttrType == Relationship) nodeP->name = (char*) "object"; + else if (subAttrType == LanguageProperty) nodeP->name = (char*) "languageMap"; + else if (subAttrType == VocabularyProperty) nodeP->name = (char*) "vocab"; kjChildAdd(subAttrP, nodeP); } diff --git a/src/lib/orionld/notifications/notificationSend.cpp b/src/lib/orionld/notifications/notificationSend.cpp index 95cbc2d271..b1d3683e28 100644 --- a/src/lib/orionld/notifications/notificationSend.cpp +++ b/src/lib/orionld/notifications/notificationSend.cpp @@ -259,8 +259,26 @@ static void attributeFix(KjNode* attrP, CachedSubscription* subP) if (addedP != NULL) kjChildRemove(attrP, addedP); if (removedP != NULL) kjChildRemove(attrP, removedP); - bool asSimplified = false; + // + // If vocab-property, its value needs to be compacted + // + KjNode* vocabP = kjLookup(attrP, "vocab"); + if (vocabP != NULL) + { + if (vocabP->type == KjString) + vocabP->value.s = orionldContextItemAliasLookup(subP->contextP, vocabP->value.s, NULL, NULL); + else if (vocabP->type == KjArray) + { + for (KjNode* wordP = vocabP->value.firstChildP; wordP != NULL; wordP = wordP->next) + { + if (wordP->type == KjString) + wordP->value.s = orionldContextItemAliasLookup(subP->contextP, wordP->value.s, NULL, NULL); + } + } + } + + bool asSimplified = false; if (attrP->type == KjObject) { if (simplified) attributeToSimplified(attrP, subP->lang.c_str()); @@ -284,6 +302,7 @@ static void attributeFix(KjNode* attrP, CachedSubscription* subP) if (strcmp(saP->name, "value") == 0) continue; if (strcmp(saP->name, "object") == 0) continue; if (strcmp(saP->name, "languageMap") == 0) continue; + if (strcmp(saP->name, "vocab") == 0) continue; if (strcmp(saP->name, "unitCode") == 0) continue; eqForDot(saP->name); diff --git a/src/lib/orionld/payloadCheck/pCheckAttribute.cpp b/src/lib/orionld/payloadCheck/pCheckAttribute.cpp index a4a78b247a..dcdc3a1296 100644 --- a/src/lib/orionld/payloadCheck/pCheckAttribute.cpp +++ b/src/lib/orionld/payloadCheck/pCheckAttribute.cpp @@ -44,6 +44,7 @@ extern "C" #include "orionld/common/orionldState.h" // orionldState #include "orionld/common/orionldError.h" // orionldError #include "orionld/common/dateTime.h" // dateTimeFromString +#include "orionld/context/orionldContextItemExpand.h" // orionldContextItemExpand #include "orionld/context/orionldAttributeExpand.h" // orionldAttributeExpand #include "orionld/context/orionldSubAttributeExpand.h" // orionldSubAttributeExpand #include "orionld/serviceRoutines/orionldPatchEntity2.h" // orionldPatchEntity2 @@ -65,27 +66,38 @@ static const char* attrTypeChangeTitle(OrionldAttributeType oldType, OrionldAttr { if (newType == Property) { - if (oldType == Relationship) return "Attempt to transform a Relationship into a Property"; - if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a Property"; - if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a Property"; + if (oldType == Relationship) return "Attempt to transform a Relationship into a Property"; + if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a Property"; + if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a Property"; + if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a Property"; } else if (newType == Relationship) { - if (oldType == Property) return "Attempt to transform a Property into a Relationship"; - if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a Relationship"; - if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a Relationship"; + if (oldType == Property) return "Attempt to transform a Property into a Relationship"; + if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a Relationship"; + if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a Relationship"; + if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a Relationship"; } else if (newType == GeoProperty) { - if (oldType == Property) return "Attempt to transform a Property into a GeoProperty"; - if (oldType == Relationship) return "Attempt to transform a Relationship into a GeoProperty"; - if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a GeoProperty"; + if (oldType == Property) return "Attempt to transform a Property into a GeoProperty"; + if (oldType == Relationship) return "Attempt to transform a Relationship into a GeoProperty"; + if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a GeoProperty"; + if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a GeoProperty"; } else if (newType == LanguageProperty) { - if (oldType == Property) return "Attempt to transform a Property into a LanguageProperty"; - if (oldType == Relationship) return "Attempt to transform a Relationship into a LanguageProperty"; - if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a LanguageProperty"; + if (oldType == Property) return "Attempt to transform a Property into a LanguageProperty"; + if (oldType == Relationship) return "Attempt to transform a Relationship into a LanguageProperty"; + if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a LanguageProperty"; + if (oldType == VocabularyProperty) return "Attempt to transform a VocabularyProperty into a LanguageProperty"; + } + else if (newType == VocabularyProperty) + { + if (oldType == Property) return "Attempt to transform a Property into a VocabularyProperty"; + if (oldType == Relationship) return "Attempt to transform a Relationship into a VocabularyProperty"; + if (oldType == GeoProperty) return "Attempt to transform a GeoProperty into a VocabularyProperty"; + if (oldType == LanguageProperty) return "Attempt to transform a LanguageProperty into a GeoProperty"; } return "Attribute type inconsistency"; @@ -359,6 +371,7 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a KjNode* valueP = kjLookup(attrP, "value"); KjNode* objectP = kjLookup(attrP, "object"); KjNode* languageMapP = kjLookup(attrP, "languageMap"); + KjNode* vocabP = kjLookup(attrP, "vocab"); if (attributeType == Property) { @@ -372,6 +385,11 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a Property: languageMap", attrP->name, 400); return false; } + else if (vocabP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a Property: vocab", attrP->name, 400); + return false; + } else if ((valueP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /value/ field for Property at creation time", attrP->name, 400); @@ -390,6 +408,11 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a GeoProperty: languageMap", attrP->name, 400); return false; } + else if (vocabP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a GeoProperty: vocab", attrP->name, 400); + return false; + } else if ((valueP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /value/ field for GeoProperty at creation time", attrP->name, 400); @@ -408,6 +431,11 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a Relationship: languageMap", attrP->name, 400); return false; } + else if (vocabP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a Relationship: vocab", attrP->name, 400); + return false; + } else if ((objectP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /object/ field for Relationship at creation time", attrP->name, 400); @@ -426,12 +454,40 @@ bool valueAndTypeCheck(KjNode* attrP, OrionldAttributeType attributeType, bool a orionldError(OrionldBadRequestData, "Forbidden field for a LanguageProperty: object", attrP->name, 400); return false; } + else if (vocabP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a LanguageProperty: vocab", attrP->name, 400); + return false; + } else if ((languageMapP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing { orionldError(OrionldBadRequestData, "Missing /languageMap/ field for LanguageProperty at creation time", attrP->name, 400); return false; } } + else if (attributeType == VocabularyProperty) + { + if (valueP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a VocabularyProperty: value", attrP->name, 400); + return false; + } + else if (objectP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a VocabularyProperty: object", attrP->name, 400); + return false; + } + else if (languageMapP != NULL) + { + orionldError(OrionldBadRequestData, "Forbidden field for a VocabularyProperty: languageMap", attrP->name, 400); + return false; + } + else if ((vocabP == NULL) && (attributeExisted == false)) // Attribute is new but the value is missing + { + orionldError(OrionldBadRequestData, "Missing /vocab/ field for VocabularyProperty at creation time", attrP->name, 400); + return false; + } + } return true; } @@ -500,7 +556,6 @@ bool datasetIdCheck(KjNode* datasetIdP) -static bool objectArrayCheck(KjNode* arrayP); // ----------------------------------------------------------------------------- // // objectCheck - @@ -514,12 +569,24 @@ static bool objectCheck(KjNode* objectP) } else if (objectP->type == KjArray) { - if (objectArrayCheck(objectP) == false) - return false; + for (KjNode* uriP = objectP->value.firstChildP; uriP != NULL; uriP = uriP->next) + { + if (uriP->type != KjString) + { + orionldError(OrionldBadRequestData, "Invalid Relationship object array item - not a String", objectP->name, 400); + return false; + } + + if (pCheckUri(uriP->value.s, objectP->name, true) == false) + { + orionldError(OrionldBadRequestData, "Invalid Relationship object array item - not a URI", uriP->value.s, 400); + return false; + } + } } else { - orionldError(OrionldBadRequestData, "Invalid JSON type - not a string nor an array", objectP->name, 400); + orionldError(OrionldBadRequestData, "Invalid Relationship object - not a string nor an array", objectP->name, 400); return false; } @@ -530,19 +597,33 @@ static bool objectCheck(KjNode* objectP) // ----------------------------------------------------------------------------- // -// objectArrayCheck - -// -// NOTE -// A Relationship must have an "object field that is a string that is a valid URI". -// However, Orion-LD (against the ETSI NGSI-LD API spec) allows for the "object" field to also be an array of URIs. -// Once ListRelationahip is implemented, this will be deprecated. +// pCheckVocabulary - // -static bool objectArrayCheck(KjNode* arrayP) +static bool pCheckVocabulary(KjNode* vocabP, const char* attrName) { - for (KjNode* uriP = arrayP->value.firstChildP; uriP != NULL; uriP = uriP->next) + if (vocabP->type == KjString) { - if (objectCheck(uriP) == false) - return false; + vocabP->value.s = orionldContextItemExpand(orionldState.contextP, vocabP->value.s, true, NULL); + return true; + } + + if (vocabP->type == KjArray) + { + for (KjNode* wordP = vocabP->value.firstChildP; wordP != NULL; wordP = wordP->next) + { + if (wordP->type != KjString) + { + orionldError(OrionldBadRequestData, "Invalid VocabularyProperty vocab array item - not a string", attrName, 400); + return false; + } + + wordP->value.s = orionldContextItemExpand(orionldState.contextP, wordP->value.s, true, NULL); + } + } + else + { + orionldError(OrionldBadRequestData, "Invalid VocabularyProperty vocab - not a string nor an array", attrName, 400); + return false; } return true; @@ -693,6 +774,15 @@ bool deletionWithTypePresent(KjNode* attrP, KjNode* typeP) return true; } } + else if (strcmp(typeP->value.s, "VocabularyProperty") == 0) + { + valueP = kjLookup(attrP, "vocab"); + if ((valueP != NULL) && (valueP->type == KjString) && (strcmp(valueP->value.s, "urn:ngsi-ld:null") == 0)) + { + attrP->type = KjNull; + return true; + } + } else if (strcmp(typeP->value.s, "LanguageProperty") == 0) { valueP = kjLookup(attrP, "languageMap"); @@ -716,13 +806,14 @@ bool deletionWithTypePresent(KjNode* attrP, KjNode* typeP) // // deletionWithoutTypePresent - // -bool deletionWithoutTypePresent +static bool deletionWithoutTypePresent ( KjNode* attrP, OrionldAttributeType attributeType, KjNode* valueP, KjNode* objectP, - KjNode* languageMapP + KjNode* languageMapP, + KjNode* vocabP ) { if ((attributeType == Property) || (attributeType == GeoProperty)) @@ -741,6 +832,14 @@ bool deletionWithoutTypePresent return true; } } + else if (attributeType == VocabularyProperty) + { + if ((vocabP != NULL) && (vocabP->type == KjString) && (strcmp(vocabP->value.s, "urn:ngsi-ld:null") == 0)) + { + attrP->type = KjNull; + return true; + } + } else if (attributeType == LanguageProperty) { if ((languageMapP != NULL) && (languageMapP->type == KjObject)) @@ -916,6 +1015,7 @@ static bool pCheckAttributeObject KjNode* valueP = kjLookup(attrP, "value"); KjNode* objectP = kjLookup(attrP, "object"); KjNode* languageMapP = kjLookup(attrP, "languageMap"); + KjNode* vocabP = kjLookup(attrP, "vocab"); if (valueP != NULL) { @@ -928,6 +1028,8 @@ static bool pCheckAttributeObject attributeType = Relationship; else if (languageMapP != NULL) attributeType = LanguageProperty; + else if (vocabP != NULL) + attributeType = VocabularyProperty; else { // If new attribute and no value field at all - error @@ -958,7 +1060,7 @@ static bool pCheckAttributeObject // if ((orionldState.serviceP->options & ORIONLD_SERVICE_OPTION_ACCEPT_JSONLD_NULL) != 0) { - if (deletionWithoutTypePresent(attrP, attributeType, valueP, objectP, languageMapP) == true) + if (deletionWithoutTypePresent(attrP, attributeType, valueP, objectP, languageMapP, vocabP) == true) return true; } } @@ -1005,7 +1107,7 @@ static bool pCheckAttributeObject fieldP->value = fieldP->value.firstChildP->value; } } - if ((attributeType == Relationship) || (attributeType == LanguageProperty)) + if ((attributeType == Relationship) || (attributeType == LanguageProperty) || (attributeType == VocabularyProperty)) { orionldError(OrionldBadRequestData, "Invalid member /value/", "valid for Property/GeoProperty attributes only", 400); return false; @@ -1019,7 +1121,6 @@ static bool pCheckAttributeObject { if (attributeType == Relationship) { - // Until datasetId is fully implemented - string array allowed for Relationship if (objectCheck(fieldP) == false) return false; @@ -1037,6 +1138,11 @@ static bool pCheckAttributeObject if (pCheckLanguageMap(fieldP, attrP->name) == false) return false; } + else if ((attributeType == VocabularyProperty) && (strcmp(fieldP->name, "vocab") == 0)) + { + if (pCheckVocabulary(fieldP, attrP->name) == false) + return false; + } else if (strcmp(fieldP->name, "observedAt") == 0) { // @@ -1233,6 +1339,12 @@ static bool validAttrName(const char* attrName, bool isAttribute) // - observedAt // - datasetId (sub-attributes don't have datasetId) // +// - Attributes of type VocabularyProperty can have the following special attributes: +// - type +// - vocab +// - observedAt +// - datasetId (sub-attributes don't have datasetId) +// bool pCheckAttribute ( const char* entityId, diff --git a/src/lib/orionld/types/OrionldAttributeType.cpp b/src/lib/orionld/types/OrionldAttributeType.cpp index 6041aa2d91..ae22d51e6e 100644 --- a/src/lib/orionld/types/OrionldAttributeType.cpp +++ b/src/lib/orionld/types/OrionldAttributeType.cpp @@ -38,11 +38,12 @@ const char* orionldAttributeTypeName(OrionldAttributeType attributeType) { switch (attributeType) { - case NoAttributeType: return "NoAttributeType"; - case Property: return "Property"; - case Relationship: return "Relationship"; - case GeoProperty: return "GeoProperty"; - case LanguageProperty: return "LanguageProperty"; + case NoAttributeType: return "NoAttributeType"; + case Property: return "Property"; + case Relationship: return "Relationship"; + case GeoProperty: return "GeoProperty"; + case LanguageProperty: return "LanguageProperty"; + case VocabularyProperty: return "VocabularyProperty"; } return "InvalidAttributeType"; @@ -56,10 +57,11 @@ const char* orionldAttributeTypeName(OrionldAttributeType attributeType) // OrionldAttributeType orionldAttributeType(const char* typeString) { - if (strcmp(typeString, "Property") == 0) return Property; - else if (strcmp(typeString, "Relationship") == 0) return Relationship; - else if (strcmp(typeString, "GeoProperty") == 0) return GeoProperty; - else if (strcmp(typeString, "LanguageProperty") == 0) return LanguageProperty; + if (strcmp(typeString, "Property") == 0) return Property; + else if (strcmp(typeString, "Relationship") == 0) return Relationship; + else if (strcmp(typeString, "GeoProperty") == 0) return GeoProperty; + else if (strcmp(typeString, "LanguageProperty") == 0) return LanguageProperty; + else if (strcmp(typeString, "VocabularyProperty") == 0) return VocabularyProperty; return NoAttributeType; } diff --git a/src/lib/orionld/types/OrionldAttributeType.h b/src/lib/orionld/types/OrionldAttributeType.h index e12ae26dd7..8141fc4e02 100644 --- a/src/lib/orionld/types/OrionldAttributeType.h +++ b/src/lib/orionld/types/OrionldAttributeType.h @@ -38,7 +38,8 @@ typedef enum OrionldAttributeType Property, Relationship, GeoProperty, - LanguageProperty + LanguageProperty, + VocabularyProperty } OrionldAttributeType; diff --git a/test/functionalTest/cases/0000_ngsild/ngsild_new_entity_create-error-handling.test b/test/functionalTest/cases/0000_ngsild/ngsild_new_entity_create-error-handling.test index 1beaa91749..1e5e4b308d 100644 --- a/test/functionalTest/cases/0000_ngsild/ngsild_new_entity_create-error-handling.test +++ b/test/functionalTest/cases/0000_ngsild/ngsild_new_entity_create-error-handling.test @@ -550,13 +550,13 @@ Date: REGEX(.*) 21. Relationship whose object is not a string - normalized ========================================================== HTTP/1.1 400 Bad Request -Content-Length: 135 +Content-Length: 145 Content-Type: application/json Date: REGEX(.*) { "detail": "object", - "title": "Invalid JSON type - not a string nor an array", + "title": "Invalid Relationship object - not a string nor an array", "type": "https://uri.etsi.org/ngsi-ld/errors/BadRequestData" } @@ -564,13 +564,13 @@ Date: REGEX(.*) 22. Relationship whose object is not a string - concise ======================================================= HTTP/1.1 400 Bad Request -Content-Length: 135 +Content-Length: 145 Content-Type: application/json Date: REGEX(.*) { "detail": "object", - "title": "Invalid JSON type - not a string nor an array", + "title": "Invalid Relationship object - not a string nor an array", "type": "https://uri.etsi.org/ngsi-ld/errors/BadRequestData" } diff --git a/test/functionalTest/cases/0000_ngsild/ngsild_simplified_POST_Entities_ERRORS.test b/test/functionalTest/cases/0000_ngsild/ngsild_simplified_POST_Entities_ERRORS.test index 220e7f2ad3..74b8fd78c8 100644 --- a/test/functionalTest/cases/0000_ngsild/ngsild_simplified_POST_Entities_ERRORS.test +++ b/test/functionalTest/cases/0000_ngsild/ngsild_simplified_POST_Entities_ERRORS.test @@ -863,13 +863,13 @@ Date: REGEX(.*) 41. POST /ngsi-ld/v1/entities, with a Relationship R1 whose object is a Number - see error ========================================================================================== HTTP/1.1 400 Bad Request -Content-Length: 135 +Content-Length: 145 Content-Type: application/json Date: REGEX(.*) { "detail": "object", - "title": "Invalid JSON type - not a string nor an array", + "title": "Invalid Relationship object - not a string nor an array", "type": "https://uri.etsi.org/ngsi-ld/errors/BadRequestData" } diff --git a/test/functionalTest/cases/0000_ngsild/ngsild_vocab-property.test b/test/functionalTest/cases/0000_ngsild/ngsild_vocab-property.test new file mode 100644 index 0000000000..eb41c70cd0 --- /dev/null +++ b/test/functionalTest/cases/0000_ngsild/ngsild_vocab-property.test @@ -0,0 +1,320 @@ +# CopyrightÂș 2024 FIWARE Foundation e.V. +# +# This file is part of Orion-LD Context Broker. +# +# Orion-LD Context Broker is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Orion-LD Context Broker is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +# General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Orion-LD Context Broker. If not, see http://www.gnu.org/licenses/. +# +# For those usages not covered by this license please contact with +# orionld at fiware dot org + +# VALGRIND_READY - to mark the test ready for valgrindTestSuite.sh + +--NAME-- +VocabularyProperty + +--SHELL-INIT-- +dbInit CB + +orionldStart CB -experimental +accumulatorStart --pretty-print + +--SHELL-- + +# +# 01. Create a subscription on entity type T +# 02. Create an entity urn:E1, type T, with a VocabularyProperty V1: "abc" +# 03. Create an entity urn:E2, type T, with a VocabularyProperty V2: [ "vocab", "id" ] +# 04. GET urn:E1 to see V1 +# 04b. See urn:E1 in the DB - see "abc" expanded +# 05. GET urn:E2 to see V2 +# 05b. See urn:E2 in the DB - see [ "vocab", "id" ] expanded +# 06. Dump/Reset accumulator, see urn:E1+V1 and urn:E2+V2 +# + +echo '01. Create a subscription on entity type T' +echo '==========================================' +payload='{ + "id": "urn:ngsi-ld:subs:S1", + "type": "Subscription", + "entities": [ + { + "type": "T" + } + ], + "notification": { + "endpoint": { + "uri": "http://127.0.0.1:'${LISTENER_PORT}'/notify" + }, + "showChanges": true + } +}' +orionCurl --url /ngsi-ld/v1/subscriptions --payload "$payload" +echo +echo + + +echo '02. Create an entity urn:E1, type T, with a VocabularyProperty V1: "abc"' +echo '========================================================================' +payload='{ + "id": "urn:E1", + "type": "T", + "V1": { "vocab": "abc" } +}' +orionCurl --url /ngsi-ld/v1/entities --payload "$payload" +echo +echo + + +echo '03. Create an entity urn:E2, type T, with a VocabularyProperty V2: [ "vocab", "id" ]' +echo '====================================================================================' +payload='{ + "id": "urn:E2", + "type": "T", + "V2": { "vocab": [ "vocab", "id" ] } +}' +orionCurl --url /ngsi-ld/v1/entities --payload "$payload" +echo +echo + + +echo '04. GET urn:E1 to see V1' +echo '========================' +orionCurl --url /ngsi-ld/v1/entities/urn:E1 +echo +echo + + +echo '04b. See urn:E1 in the DB - see "abc" expanded' +echo '==============================================' +mongoCmd2 ftest 'db.entities.findOne({"_id.id": "urn:E1"})' +echo +echo + + +echo '05. GET urn:E2 to see V2' +echo '========================' +orionCurl --url /ngsi-ld/v1/entities/urn:E2 +echo +echo + + +echo '05b. See urn:E2 in the DB - see [ "vocab", "id" ] expanded' +echo '==========================================================' +mongoCmd2 ftest 'db.entities.findOne({"_id.id": "urn:E2"})' +echo +echo + + +echo "06. Dump/Reset accumulator, see urn:E1+V1 and urn:E2+V2" +echo "=======================================================" +sleep .2 +accumulatorDump +accumulatorReset +echo +echo + + +--REGEXPECT-- +01. Create a subscription on entity type T +========================================== +HTTP/1.1 201 Created +Content-Length: 0 +Date: REGEX(.*) +Location: /ngsi-ld/v1/subscriptions/urn:ngsi-ld:subs:S1 + + + +02. Create an entity urn:E1, type T, with a VocabularyProperty V1: "abc" +======================================================================== +HTTP/1.1 201 Created +Content-Length: 0 +Date: REGEX(.*) +Location: /ngsi-ld/v1/entities/urn:E1 + + + +03. Create an entity urn:E2, type T, with a VocabularyProperty V2: [ "vocab", "id" ] +==================================================================================== +HTTP/1.1 201 Created +Content-Length: 0 +Date: REGEX(.*) +Location: /ngsi-ld/v1/entities/urn:E2 + + + +04. GET urn:E1 to see V1 +======================== +HTTP/1.1 200 OK +Content-Length: 75 +Content-Type: application/json +Date: REGEX(.*) +Link: