diff --git a/build.gradle.kts b/build.gradle.kts
index 5b1074a5e..4436971f4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -79,6 +79,7 @@ subprojects {
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
testImplementation("org.springframework.boot:spring-boot-starter-test")
+ testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("io.projectreactor:reactor-test")
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation("org.springframework.security:spring-security-test")
diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml
index 7f380d5b4..b2b802489 100644
--- a/search-service/config/detekt/baseline.xml
+++ b/search-service/config/detekt/baseline.xml
@@ -5,13 +5,13 @@
ClassNaming:V0_29_JsonLd_migrationTests.kt$V0_29_JsonLd_migrationTests
ClassNaming:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration : BaseJavaMigration
ComplexCondition:EntitiesQueryUtils.kt$geoQuery == null && q.isNullOrEmpty() && typeSelection.isNullOrEmpty() && attrs.isEmpty()
- ComplexCondition:EntityQueryService.kt$EntityQueryService$it && !inverse || !it && inverse
Filename:V0_29__JsonLd_migration.kt$db.migration.V0_29__JsonLd_migration.kt
LongMethod:AttributeInstanceService.kt$AttributeInstanceService$@Transactional suspend fun create(attributeInstance: AttributeInstance): Either<APIException, Unit>
LongMethod:EnabledAuthorizationServiceTests.kt$EnabledAuthorizationServiceTests$@Test fun `it should return serialized access control entities with other rigths if user is owner`()
LongMethod:EntityAccessControlHandler.kt$EntityAccessControlHandler$@PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun addRightsOnEntities( @RequestHeader httpHeaders: HttpHeaders, @PathVariable subjectId: String, @RequestBody requestBody: Mono<String>, @AllowedParameters @RequestParam queryParams: MultiValueMap<String, String> ): ResponseEntity<*>
LongMethod:EntityHandler.kt$EntityHandler$@GetMapping("/{entityId}", produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE]) suspend fun getByURI( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @AllowedParameters( implemented = [ QP.OPTIONS, QP.TYPE, QP.ATTRS, QP.GEOMETRY_PROPERTY, QP.LANG, QP.CONTAINED_BY, QP.JOIN, QP.JOIN_LEVEL, QP.DATASET_ID, ], notImplemented = [QP.FORMAT, QP.PICK, QP.OMIT, QP.ENTITY_MAP, QP.LOCAL, QP.VIA] ) @RequestParam queryParams: MultiValueMap<String, String> ): ResponseEntity<*>
LongMethod:EntityHandler.kt$EntityHandler$@GetMapping(produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, @AllowedParameters( implemented = [ QP.OPTIONS, QP.COUNT, QP.OFFSET, QP.LIMIT, QP.ID, QP.TYPE, QP.ID_PATTERN, QP.ATTRS, QP.Q, QP.GEOMETRY, QP.GEOREL, QP.COORDINATES, QP.GEOPROPERTY, QP.GEOMETRY_PROPERTY, QP.LANG, QP.SCOPEQ, QP.CONTAINED_BY, QP.JOIN, QP.JOIN_LEVEL, QP.DATASET_ID, ], notImplemented = [QP.FORMAT, QP.PICK, QP.OMIT, QP.EXPAND_VALUES, QP.CSF, QP.ENTITY_MAP, QP.LOCAL, QP.VIA] ) @RequestParam queryParams: MultiValueMap<String, String> ): ResponseEntity<*>
+ LongMethod:EntityEventService.kt$EntityEventService$private fun publishAttributeChangeEvent( updatedDetails: UpdatedDetails, sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair<List<ExpandedTerm>, String>, serializedAttribute: Pair<ExpandedTerm, String>, overwrite: Boolean )
LongMethod:LinkedEntityServiceTests.kt$LinkedEntityServiceTests$@Test fun `it should inline entities up to the asked 2nd level`()
LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun mergePatchProvider(): Stream<Arguments>
LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun partialUpdatePatchProvider(): Stream<Arguments>
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt
index d4fa226e6..a7e32bec2 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/listener/IAMListener.kt
@@ -125,7 +125,7 @@ class IAMListener(
// (if it no longer exists, it fails because of access rights checks)
if (searchProperties.onOwnerDeleteCascadeEntities && subjectType == SubjectType.USER) {
entityAccessRightsService.getEntitiesIdsOwnedBySubject(sub).getOrNull()?.forEach { entityId ->
- entityService.deleteEntity(entityId, sub)
+ entityService.permanentlyDeleteEntity(entityId, sub)
}
Unit.right()
} else Unit.right()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt
index 022110962..7528f783c 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/model/EntityAccessRights.kt
@@ -6,6 +6,7 @@ import com.egm.stellio.shared.model.addNonReifiedProperty
import com.egm.stellio.shared.model.addSubAttribute
import com.egm.stellio.shared.util.AccessRight
import com.egm.stellio.shared.util.AuthContextModel
+import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_IS_DELETED
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_RIGHT
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SAP
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SUBJECT_INFO
@@ -27,6 +28,7 @@ import java.net.URI
data class EntityAccessRights(
val id: URI,
val types: List,
+ val isDeleted: Boolean = false,
// right the current user has on the entity
val right: AccessRight,
val specificAccessPolicy: AuthContextModel.SpecificAccessPolicy? = null,
@@ -55,6 +57,8 @@ data class EntityAccessRights(
resultEntity[JSONLD_ID] = id.toString()
resultEntity[JSONLD_TYPE] = types
+ if (isDeleted)
+ resultEntity[AUTH_PROP_IS_DELETED] = buildExpandedPropertyValue(true)
resultEntity[AUTH_PROP_RIGHT] = buildExpandedPropertyValue(right.attributeName)
specificAccessPolicy?.run {
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt
index c0dd9d0fe..c79064cfa 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/AuthorizationService.kt
@@ -23,6 +23,7 @@ interface AuthorizationService {
suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean,
contexts: List,
sub: Option
): Either>>
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt
index f27debb04..5b56b8a42 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/DisabledAuthorizationService.kt
@@ -41,6 +41,7 @@ class DisabledAuthorizationService : AuthorizationService {
override suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean,
contexts: List,
sub: Option
): Either>> = Pair(-1, emptyList()).right()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt
index 53e3e31cf..65a64a3cf 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationService.kt
@@ -108,6 +108,7 @@ class EnabledAuthorizationService(
override suspend fun getAuthorizedEntities(
entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean,
contexts: List,
sub: Option
): Either>> = either {
@@ -115,9 +116,8 @@ class EnabledAuthorizationService(
val entitiesAccessRights = entityAccessRightsService.getSubjectAccessRights(
sub,
accessRights,
- entitiesQuery.typeSelection,
- entitiesQuery.ids,
- entitiesQuery.paginationQuery
+ entitiesQuery,
+ includeDeleted
).bind()
// for each entity user is admin or creator of, retrieve the full details of rights other users have on it
@@ -148,7 +148,8 @@ class EnabledAuthorizationService(
sub,
accessRights,
entitiesQuery.typeSelection,
- entitiesQuery.ids
+ entitiesQuery.ids,
+ includeDeleted
).bind()
Pair(count, entitiesAccessControlWithSubjectRights)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt
index e85454db8..fb6b3ce12 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt
@@ -18,13 +18,13 @@ import com.egm.stellio.search.common.util.toJsonString
import com.egm.stellio.search.common.util.toList
import com.egm.stellio.search.common.util.toOptionalEnum
import com.egm.stellio.search.common.util.toUri
+import com.egm.stellio.search.entity.model.EntitiesQueryFromGet
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.model.EntityTypeSelection
import com.egm.stellio.shared.model.NgsiLdAttribute
import com.egm.stellio.shared.model.ResourceNotFoundException
-import com.egm.stellio.shared.queryparameter.PaginationQuery
import com.egm.stellio.shared.util.AccessRight
import com.egm.stellio.shared.util.AccessRight.CAN_ADMIN
import com.egm.stellio.shared.util.AccessRight.CAN_READ
@@ -220,30 +220,32 @@ class EntityAccessRightsService(
suspend fun getSubjectAccessRights(
sub: Option,
accessRights: List,
- type: EntityTypeSelection? = null,
- ids: Set? = null,
- paginationQuery: PaginationQuery,
+ entitiesQuery: EntitiesQueryFromGet,
+ includeDeleted: Boolean = false
): Either> = either {
+ val ids = entitiesQuery.ids
+ val typeSelection = entitiesQuery.typeSelection
val subjectUuids = subjectReferentialService.getSubjectAndGroupsUUID(sub).bind()
val isStellioAdmin = subjectReferentialService.hasStellioAdminRole(subjectUuids).bind()
databaseClient
.sql(
"""
- SELECT ep.entity_id, ep.types, ear.access_right, ep.specific_access_policy
+ SELECT ep.entity_id, ep.types, ear.access_right, ep.specific_access_policy, ep.deleted_at
FROM entity_access_rights ear
LEFT JOIN entity_payload ep ON ear.entity_id = ep.entity_id
WHERE ${if (isStellioAdmin) "1 = 1" else "subject_id IN (:subject_uuids)" }
${if (accessRights.isNotEmpty()) " AND access_right IN (:access_rights)" else ""}
- ${if (!type.isNullOrEmpty()) " AND (${buildTypeQuery(type)})" else ""}
- ${if (!ids.isNullOrEmpty()) " AND ear.entity_id IN (:entities_ids)" else ""}
+ ${if (!typeSelection.isNullOrEmpty()) " AND (${buildTypeQuery(typeSelection)})" else ""}
+ ${if (ids.isNotEmpty()) " AND ear.entity_id IN (:entities_ids)" else ""}
+ ${if (!includeDeleted) " AND deleted_at IS NULL" else ""}
ORDER BY entity_id
LIMIT :limit
OFFSET :offset;
""".trimIndent()
)
- .bind("limit", paginationQuery.limit)
- .bind("offset", paginationQuery.offset)
+ .bind("limit", entitiesQuery.paginationQuery.limit)
+ .bind("offset", entitiesQuery.paginationQuery.offset)
.let {
if (!isStellioAdmin)
it.bind("subject_uuids", subjectUuids)
@@ -255,7 +257,7 @@ class EntityAccessRightsService(
else it
}
.let {
- if (!ids.isNullOrEmpty())
+ if (ids.isNotEmpty())
it.bind("entities_ids", ids)
else it
}
@@ -268,6 +270,7 @@ class EntityAccessRightsService(
EntityAccessRights(
ear.id,
ear.types,
+ ear.isDeleted,
entityAccessRights.maxOf { it.right },
ear.specificAccessPolicy
)
@@ -278,7 +281,8 @@ class EntityAccessRightsService(
sub: Option,
accessRights: List,
type: EntityTypeSelection? = null,
- ids: Set? = null
+ ids: Set? = null,
+ includeDeleted: Boolean = false
): Either = either {
val subjectUuids = subjectReferentialService.getSubjectAndGroupsUUID(sub).bind()
val isStellioAdmin = subjectReferentialService.hasStellioAdminRole(subjectUuids).bind()
@@ -293,6 +297,7 @@ class EntityAccessRightsService(
${if (accessRights.isNotEmpty()) " AND access_right IN (:access_rights)" else ""}
${if (!type.isNullOrEmpty()) " AND (${buildTypeQuery(type)})" else ""}
${if (!ids.isNullOrEmpty()) " AND ear.entity_id IN (:entities_ids)" else ""}
+ ${if (!includeDeleted) " AND deleted_at IS NULL" else ""}
""".trimIndent()
)
.let {
@@ -443,6 +448,7 @@ class EntityAccessRightsService(
return EntityAccessRights(
id = toUri(row["entity_id"]),
types = toList(row["types"]),
+ isDeleted = row["deleted_at"] != null,
right = accessRight,
specificAccessPolicy = toOptionalEnum(row["specific_access_policy"])
)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
index 996dd3985..7de1a6867 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/web/EntityAccessControlHandler.kt
@@ -19,6 +19,7 @@ import com.egm.stellio.shared.model.toNgsiLdAttribute
import com.egm.stellio.shared.model.toNgsiLdAttributes
import com.egm.stellio.shared.queryparameter.AllowedParameters
import com.egm.stellio.shared.queryparameter.QP
+import com.egm.stellio.shared.queryparameter.QueryParameter
import com.egm.stellio.shared.util.AccessRight
import com.egm.stellio.shared.util.AuthContextModel.ALL_ASSIGNABLE_IAM_RIGHTS
import com.egm.stellio.shared.util.AuthContextModel.ALL_IAM_RIGHTS
@@ -70,10 +71,11 @@ class EntityAccessControlHandler(
@GetMapping("/entities", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE])
suspend fun getAuthorizedEntities(
@RequestHeader httpHeaders: HttpHeaders,
- @AllowedParameters(implemented = [QP.ID, QP.TYPE, QP.ATTRS, QP.COUNT, QP.OFFSET, QP.LIMIT])
+ @AllowedParameters(implemented = [QP.ID, QP.TYPE, QP.ATTRS, QP.COUNT, QP.OFFSET, QP.LIMIT, QP.INCLUDE_DELETED])
@RequestParam queryParams: MultiValueMap
): ResponseEntity<*> = either {
val sub = getSubFromSecurityContext()
+ val includeDeleted = queryParams.getFirst(QueryParameter.INCLUDE_DELETED.key)?.toBoolean() == true
val contexts = getAuthzContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts).bind()
val mediaType = getApplicableMediaType(httpHeaders).bind()
@@ -91,6 +93,7 @@ class EntityAccessControlHandler(
val (count, entities) = authorizationService.getAuthorizedEntities(
entitiesQuery,
+ includeDeleted,
contexts,
sub
).bind()
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt
index eb9aecf56..c2d1720b6 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/AttributeService.kt
@@ -30,6 +30,7 @@ class AttributeService(
"""
SELECT DISTINCT(attribute_name)
FROM temporal_entity_attribute
+ WHERE deleted_at IS NULL
ORDER BY attribute_name
""".trimIndent()
).allToMappedList { rowToAttributeNames(it) }
@@ -42,7 +43,10 @@ class AttributeService(
"""
SELECT types, attribute_name
FROM entity_payload
- JOIN temporal_entity_attribute ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ JOIN temporal_entity_attribute
+ ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ AND temporal_entity_attribute.deleted_at IS NULL
+ WHERE entity_payload.deleted_at IS NULL
ORDER BY attribute_name
""".trimIndent()
).allToMappedList { rowToAttributeDetails(it) }.flatten().groupBy({ it.second }, { it.first }).toList()
@@ -65,11 +69,14 @@ class AttributeService(
WITH entities AS (
SELECT entity_id, attribute_name, attribute_type
FROM temporal_entity_attribute
- WHERE attribute_name = :attribute_name
+ WHERE attribute_name = :attribute_name
+ AND deleted_at IS NULL
)
SELECT attribute_name, attribute_type, types, count(distinct(attribute_name)) as attribute_count
FROM entity_payload
- JOIN entities ON entity_payload.entity_id = entities.entity_id
+ JOIN entities
+ ON entity_payload.entity_id = entities.entity_id
+ AND entity_payload.deleted_at IS NULL
GROUP BY types, attribute_name, attribute_type
""".trimIndent()
)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt
index 07626ecb7..7a4d796c6 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/service/EntityTypeService.kt
@@ -29,6 +29,7 @@ class EntityTypeService(
"""
SELECT DISTINCT(unnest(types)) as type
FROM entity_payload
+ WHERE deleted_at IS NULL
ORDER BY type
""".trimIndent()
).allToMappedList { rowToType(it) }
@@ -41,7 +42,10 @@ class EntityTypeService(
"""
SELECT unnest(types) as type, attribute_name
FROM entity_payload
- JOIN temporal_entity_attribute ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ JOIN temporal_entity_attribute
+ ON entity_payload.entity_id = temporal_entity_attribute.entity_id
+ AND temporal_entity_attribute.deleted_at IS NULL
+ WHERE entity_payload.deleted_at IS NULL
ORDER BY type
""".trimIndent()
).allToMappedList { rowToEntityType(it) }.groupBy({ it.first }, { it.second }).toList()
@@ -65,10 +69,12 @@ class EntityTypeService(
SELECT entity_id
FROM entity_payload
WHERE :type_name = any (types)
+ AND deleted_at IS NULL
)
SELECT attribute_name, attribute_type, (select count(entity_id) from entities) as entity_count
FROM temporal_entity_attribute
WHERE entity_id IN (SELECT entity_id FROM entities)
+ AND deleted_at IS NULL
GROUP BY attribute_name, attribute_type
""".trimIndent()
)
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt
index bca913e99..f3d4474af 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Attribute.kt
@@ -1,18 +1,33 @@
package com.egm.stellio.search.entity.model
import com.egm.stellio.shared.model.ExpandedTerm
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LANGUAGE
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LANGUAGEMAP_TERM
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_OBJECT
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE_TERM
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_VALUES
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUES
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUES
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NONE_TERM
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NULL
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUES
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECT
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECTS
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUES
+import com.egm.stellio.shared.util.JsonUtils.serializeObject
import io.r2dbc.postgresql.codec.Json
import org.springframework.data.annotation.Id
import java.net.URI
@@ -29,6 +44,7 @@ data class Attribute(
val datasetId: URI? = null,
val createdAt: ZonedDateTime,
val modifiedAt: ZonedDateTime? = null,
+ val deletedAt: ZonedDateTime? = null,
val payload: Json
) {
enum class AttributeValueType {
@@ -63,6 +79,21 @@ data class Attribute(
VocabProperty -> NGSILD_VOCABPROPERTY_TYPE.uri
}
+ /**
+ * Returns the expanded name of the member who holds the value of the attribute.
+ *
+ * For instance, https://uri.etsi.org/ngsi-ld/hasJSON if it is a JsonProperty
+ */
+ fun toExpandedValueMember(): String =
+ when (this) {
+ Property -> NGSILD_PROPERTY_VALUE
+ Relationship -> NGSILD_RELATIONSHIP_OBJECT
+ GeoProperty -> NGSILD_GEOPROPERTY_VALUE
+ JsonProperty -> NGSILD_JSONPROPERTY_VALUE
+ LanguageProperty -> NGSILD_LANGUAGEPROPERTY_VALUE
+ VocabProperty -> NGSILD_VOCABPROPERTY_VALUE
+ }
+
/**
* Returns the key of the member for the simplified representation of the attribute, as defined in 4.5.9
*/
@@ -75,5 +106,31 @@ data class Attribute(
LanguageProperty -> NGSILD_LANGUAGEPROPERTY_VALUES
VocabProperty -> NGSILD_VOCABPROPERTY_VALUES
}
+
+ fun toNullCompactedRepresentation(): Map =
+ when (this) {
+ Property, GeoProperty, JsonProperty, VocabProperty ->
+ mapOf(
+ JSONLD_TYPE_TERM to this.name,
+ JSONLD_VALUE_TERM to NGSILD_NULL
+ )
+ Relationship ->
+ mapOf(
+ JSONLD_TYPE_TERM to this.name,
+ JSONLD_OBJECT to NGSILD_NULL
+ )
+ LanguageProperty ->
+ mapOf(
+ JSONLD_TYPE_TERM to this.name,
+ JSONLD_LANGUAGEMAP_TERM to mapOf(NGSILD_NONE_TERM to NGSILD_NULL)
+ )
+ }
+
+ fun toNullValue(): String =
+ when (this) {
+ Property, GeoProperty, JsonProperty, VocabProperty, Relationship -> NGSILD_NULL
+ LanguageProperty ->
+ serializeObject(listOf(mapOf(JSONLD_VALUE to NGSILD_NULL, JSONLD_LANGUAGE to NGSILD_NONE_TERM)))
+ }
}
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt
index e81816479..f54601471 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/Entity.kt
@@ -1,5 +1,6 @@
package com.egm.stellio.search.entity.model
+import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ExpandedTerm
import com.egm.stellio.shared.util.AuthContextModel
import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy
@@ -7,6 +8,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_PROPERTY
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DELETED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedPropertyValue
@@ -44,4 +46,18 @@ data class Entity(
return resultEntity
}
+
+ companion object {
+
+ fun toExpandedDeletedEntity(
+ entityId: URI,
+ deletedAt: ZonedDateTime
+ ): ExpandedEntity =
+ ExpandedEntity(
+ members = mapOf(
+ JSONLD_ID to entityId,
+ NGSILD_DELETED_AT_PROPERTY to buildNonReifiedTemporalValue(deletedAt)
+ )
+ )
+ }
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
index 9ccf6a575..b854ede7d 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/UpdateResult.kt
@@ -53,6 +53,7 @@ data class UpdateAttributeResult(
UpdateOperationResult.APPENDED,
UpdateOperationResult.REPLACED,
UpdateOperationResult.UPDATED,
+ UpdateOperationResult.DELETED,
UpdateOperationResult.IGNORED
)
}
@@ -61,10 +62,11 @@ enum class UpdateOperationResult {
APPENDED,
REPLACED,
UPDATED,
+ DELETED,
IGNORED,
FAILED;
- fun isSuccessResult(): Boolean = listOf(APPENDED, REPLACED, UPDATED).contains(this)
+ fun isSuccessResult(): Boolean = listOf(APPENDED, REPLACED, UPDATED, DELETED).contains(this)
}
fun updateResultFromDetailedResult(updateStatuses: List): UpdateResult {
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
index adf1ff565..72dc47c7b 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityAttributeService.kt
@@ -28,6 +28,7 @@ import com.egm.stellio.search.entity.model.UpdateOperationResult
import com.egm.stellio.search.entity.model.UpdateResult
import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
import com.egm.stellio.search.entity.util.guessAttributeValueType
+import com.egm.stellio.search.entity.util.hasNgsiLdNullValue
import com.egm.stellio.search.entity.util.mergePatch
import com.egm.stellio.search.entity.util.partialUpdatePatch
import com.egm.stellio.search.entity.util.prepareAttributes
@@ -35,6 +36,7 @@ import com.egm.stellio.search.entity.util.toAttributeMetadata
import com.egm.stellio.search.entity.util.toExpandedAttributeInstance
import com.egm.stellio.search.temporal.model.AttributeInstance
import com.egm.stellio.search.temporal.service.AttributeInstanceService
+import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.BadRequestDataException
import com.egm.stellio.shared.model.ExpandedAttribute
@@ -56,6 +58,7 @@ import com.egm.stellio.shared.model.isAttributeOfType
import com.egm.stellio.shared.model.toNgsiLdEntity
import com.egm.stellio.shared.util.AttributeType
import com.egm.stellio.shared.util.AuthContextModel
+import com.egm.stellio.shared.util.JsonLdUtils
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
@@ -70,6 +73,7 @@ import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.egm.stellio.shared.util.Sub
import com.egm.stellio.shared.util.attributeNotFoundMessage
import com.egm.stellio.shared.util.entityNotFoundMessage
+import com.egm.stellio.shared.util.ngsiLdDateTime
import io.r2dbc.postgresql.codec.Json
import org.slf4j.LoggerFactory
import org.springframework.r2dbc.core.DatabaseClient
@@ -77,14 +81,15 @@ import org.springframework.r2dbc.core.bind
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.URI
-import java.time.ZoneOffset
import java.time.ZonedDateTime
-import java.util.UUID
+import java.util.*
+import kotlin.collections.map
@Service
class EntityAttributeService(
private val databaseClient: DatabaseClient,
- private val attributeInstanceService: AttributeInstanceService
+ private val attributeInstanceService: AttributeInstanceService,
+ private val applicationProperties: ApplicationProperties
) {
private val logger = LoggerFactory.getLogger(javaClass)
@@ -99,6 +104,12 @@ class EntityAttributeService(
VALUES
(:id, :entity_id, :attribute_name, :attribute_type, :attribute_value_type, :created_at, :dataset_id,
:payload)
+ ON CONFLICT (entity_id, attribute_name, dataset_id)
+ DO UPDATE SET deleted_at = null,
+ attribute_type = :attribute_type,
+ attribute_value_type = :attribute_value_type,
+ modified_at = :created_at,
+ payload = :payload
""".trimIndent()
)
.bind("id", attribute.id)
@@ -111,31 +122,6 @@ class EntityAttributeService(
.bind("payload", attribute.payload)
.execute()
- @Transactional
- suspend fun updateOnReplace(
- attributeUUID: UUID,
- attributeMetadata: AttributeMetadata,
- modifiedAt: ZonedDateTime,
- payload: String
- ): Either =
- databaseClient.sql(
- """
- UPDATE temporal_entity_attribute
- SET
- attribute_type = :attribute_type,
- attribute_value_type = :attribute_value_type,
- modified_at = :modified_at,
- payload = :payload
- WHERE id = :id
- """.trimIndent()
- )
- .bind("id", attributeUUID)
- .bind("attribute_type", attributeMetadata.type.toString())
- .bind("attribute_value_type", attributeMetadata.valueType.toString())
- .bind("modified_at", modifiedAt)
- .bind("payload", Json.of(payload))
- .execute()
-
@Transactional
suspend fun updateOnUpdate(
attributeUUID: UUID,
@@ -169,7 +155,7 @@ class EntityAttributeService(
contexts: List,
sub: String? = null
): Either = either {
- val createdAt = ZonedDateTime.now(ZoneOffset.UTC)
+ val createdAt = ngsiLdDateTime()
val expandedEntity = expandJsonLdEntity(payload, contexts)
val ngsiLdEntity = expandedEntity.toNgsiLdEntity().bind()
ngsiLdEntity.prepareAttributes()
@@ -268,32 +254,15 @@ class EntityAttributeService(
attributeMetadata.datasetId,
attribute.entityId
)
- updateOnReplace(
- attribute.id,
+ deleteAttribute(attribute.entityId, attribute.attributeName, attribute.datasetId, false, createdAt).bind()
+ addAttribute(
+ attribute.entityId,
+ attribute.attributeName,
attributeMetadata,
createdAt,
- serializeObject(attributePayload)
+ attributePayload,
+ sub
).bind()
-
- val attributeInstance = AttributeInstance(
- attributeUuid = attribute.id,
- timeProperty = AttributeInstance.TemporalProperty.MODIFIED_AT,
- time = createdAt,
- attributeMetadata = attributeMetadata,
- payload = attributePayload,
- sub = sub
- )
- attributeInstanceService.create(attributeInstance).bind()
-
- if (attributeMetadata.observedAt != null) {
- val attributeObservedAtInstance = AttributeInstance(
- attributeUuid = attribute.id,
- time = attributeMetadata.observedAt,
- attributeMetadata = attributeMetadata,
- payload = attributePayload
- )
- attributeInstanceService.create(attributeObservedAtInstance).bind()
- }
}
@Transactional
@@ -329,79 +298,123 @@ class EntityAttributeService(
}
@Transactional
- suspend fun deleteAttributes(entityId: URI): Either {
- val uuids = databaseClient.sql(
+ suspend fun deleteAttributes(entityId: URI, deletedAt: ZonedDateTime): Either = either {
+ val attributesToDelete = getForEntity(entityId, emptySet(), emptySet())
+ deleteSelectedAttributes(attributesToDelete, deletedAt).bind()
+ }
+
+ @Transactional
+ suspend fun deleteAttribute(
+ entityId: URI,
+ attributeName: String,
+ datasetId: URI?,
+ deleteAll: Boolean = false,
+ deletedAt: ZonedDateTime
+ ): Either = either {
+ logger.debug("Deleting attribute {} from entity {} (all: {})", attributeName, entityId, deleteAll)
+ val attributesToDelete =
+ if (deleteAll)
+ getForEntity(entityId, setOf(attributeName), emptySet())
+ else
+ listOf(getForEntityAndAttribute(entityId, attributeName, datasetId).bind())
+ deleteSelectedAttributes(attributesToDelete, deletedAt).bind()
+ }
+
+ @Transactional
+ internal suspend fun deleteSelectedAttributes(
+ attributesToDelete: List,
+ deletedAt: ZonedDateTime
+ ): Either = either {
+ if (attributesToDelete.isEmpty()) return Unit.right()
+ val attributesToDeleteWithPayload = attributesToDelete.map {
+ Triple(
+ it,
+ deletedAt,
+ JsonLdUtils.expandAttribute(
+ it.attributeName,
+ it.attributeType.toNullCompactedRepresentation(),
+ listOf(applicationProperties.contexts.core)
+ ).second[0]
+ )
+ }
+
+ databaseClient.sql(
"""
- DELETE FROM temporal_entity_attribute
- WHERE entity_id = :entity_id
- RETURNING id
+ UPDATE temporal_entity_attribute
+ SET deleted_at = new.deleted_at,
+ payload = new.payload
+ FROM (VALUES :values) AS new(uuid, deleted_at, payload)
+ WHERE temporal_entity_attribute.id = new.uuid
""".trimIndent()
)
- .bind("entity_id", entityId)
+ .bind("values", attributesToDeleteWithPayload.map { arrayOf(it.first.id, it.second, it.third.toJson()) })
.allToMappedList {
- toUuid(it["id"])
+ Triple(
+ toUuid(it["id"]),
+ Attribute.AttributeType.valueOf(it["attribute_type"] as String),
+ it["attribute_name"] as String
+ )
}
- return if (uuids.isNotEmpty())
- attributeInstanceService.deleteInstancesOfEntity(uuids)
- else Unit.right()
+ attributesToDeleteWithPayload.forEach { (attribute, deletedAt, expandedAttributePayload) ->
+ attributeInstanceService.addDeletedAttributeInstance(
+ attributeUuid = attribute.id,
+ value = attribute.attributeType.toNullValue(),
+ deletedAt = deletedAt,
+ attributeValues = expandedAttributePayload
+ ).bind()
+ }
}
@Transactional
- suspend fun deleteAttribute(
+ suspend fun permanentlyDeleteAttribute(
entityId: URI,
attributeName: String,
datasetId: URI?,
deleteAll: Boolean = false
- ): Either =
- either {
- logger.debug("Deleting attribute {} from entity {} (all: {})", attributeName, entityId, deleteAll)
- if (deleteAll) {
- attributeInstanceService.deleteAllInstancesOfAttribute(entityId, attributeName).bind()
- deleteAllInstances(entityId, attributeName).bind()
- } else {
- attributeInstanceService.deleteInstancesOfAttribute(entityId, attributeName, datasetId).bind()
- deleteSpecificInstance(entityId, attributeName, datasetId).bind()
- }
- }
+ ): Either = either {
+ logger.debug("Permanently deleting attribute {} from entity {} (all: {})", attributeName, entityId, deleteAll)
+ val attributesToDelete =
+ if (deleteAll)
+ getForEntity(entityId, setOf(attributeName), emptySet(), false)
+ else
+ listOf(getForEntityAndAttribute(entityId, attributeName, datasetId).bind())
- @Transactional
- suspend fun deleteSpecificInstance(
- entityId: URI,
- attributeName: String,
- datasetId: URI?
- ): Either =
databaseClient.sql(
"""
DELETE FROM temporal_entity_attribute
- WHERE entity_id = :entity_id
- ${datasetId.toDatasetIdFilter()}
- AND attribute_name = :attribute_name
+ WHERE id IN(:uuids)
""".trimIndent()
)
- .bind("entity_id", entityId)
- .bind("attribute_name", attributeName)
- .let {
- if (datasetId != null) it.bind("dataset_id", datasetId)
- else it
- }
+ .bind("uuids", attributesToDelete.map { it.id })
.execute()
+ if (deleteAll)
+ attributeInstanceService.deleteAllInstancesOfAttribute(entityId, attributeName).bind()
+ else
+ attributeInstanceService.deleteInstancesOfAttribute(entityId, attributeName, datasetId).bind()
+ }
+
@Transactional
- suspend fun deleteAllInstances(
+ suspend fun permanentlyDeleteAttributes(
entityId: URI,
- attributeName: String
- ): Either =
- databaseClient.sql(
+ ): Either = either {
+ logger.debug("Permanently deleting all attributes from entity {}", entityId)
+
+ val deletedTeas = databaseClient.sql(
"""
DELETE FROM temporal_entity_attribute
WHERE entity_id = :entity_id
- AND attribute_name = :attribute_name
+ RETURNING id
""".trimIndent()
)
.bind("entity_id", entityId)
- .bind("attribute_name", attributeName)
- .execute()
+ .allToMappedList { toUuid(it["id"]) }
+
+ if (deletedTeas.isNotEmpty())
+ attributeInstanceService.deleteInstancesOfEntity(deletedTeas).bind()
+ else Unit.right()
+ }
suspend fun getForEntities(
entitiesIds: List,
@@ -426,7 +439,7 @@ class EntityAttributeService(
val selectQuery =
"""
SELECT id, entity_id, attribute_name, attribute_type, attribute_value_type, created_at, modified_at,
- dataset_id, payload
+ deleted_at, dataset_id, payload
FROM temporal_entity_attribute
WHERE entity_id IN (:entities_ids)
$filterOnAttributes
@@ -440,7 +453,12 @@ class EntityAttributeService(
.allToMappedList { rowToAttribute(it) }
}
- suspend fun getForEntity(id: URI, attrs: Set, datasetIds: Set): List {
+ suspend fun getForEntity(
+ id: URI,
+ attrs: Set,
+ datasetIds: Set,
+ excludedDeleted: Boolean = true
+ ): List {
val filterOnAttributes =
if (attrs.isNotEmpty())
" AND " + attrs.joinToString(
@@ -463,6 +481,7 @@ class EntityAttributeService(
dataset_id, payload
FROM temporal_entity_attribute
WHERE entity_id = :entity_id
+ ${if (excludedDeleted) " and deleted_at is null " else ""}
$filterOnAttributes
$filterOnDatasetId
""".trimIndent()
@@ -500,33 +519,6 @@ class EntityAttributeService(
}
}
- suspend fun hasAttribute(
- id: URI,
- attributeName: String,
- datasetId: URI? = null
- ): Either {
- val selectQuery =
- """
- SELECT count(entity_id) as count
- FROM temporal_entity_attribute
- WHERE entity_id = :entity_id
- ${datasetId.toDatasetIdFilter()}
- AND attribute_name = :attribute_name
- """.trimIndent()
-
- return databaseClient
- .sql(selectQuery)
- .bind("entity_id", id)
- .bind("attribute_name", attributeName)
- .let {
- if (datasetId != null) it.bind("dataset_id", datasetId)
- else it
- }
- .oneToResult {
- it["count"] as Long == 1L
- }
- }
-
private fun rowToAttribute(row: Map) =
Attribute(
id = toUuid(row["id"]),
@@ -539,6 +531,7 @@ class EntityAttributeService(
datasetId = toOptionalUri(row["dataset_id"]),
createdAt = toZonedDateTime(row["created_at"]),
modifiedAt = toOptionalZonedDateTime(row["modified_at"]),
+ deletedAt = toOptionalZonedDateTime(row["deleted_at"]),
payload = toJson(row["payload"])
)
@@ -564,6 +557,7 @@ class EntityAttributeService(
from temporal_entity_attribute
where entity_id = :entity_id
and attribute_name = :attribute_name
+ and deleted_at is null
$datasetIdFilter
) as attributeNameExists;
""".trimIndent()
@@ -618,8 +612,7 @@ class EntityAttributeService(
UpdateAttributeResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ UpdateOperationResult.APPENDED
)
}.bind()
} else if (disallowOverwrite) {
@@ -643,8 +636,7 @@ class EntityAttributeService(
UpdateAttributeResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED,
- null
+ UpdateOperationResult.REPLACED
)
}.bind()
}
@@ -670,10 +662,11 @@ class EntityAttributeService(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId
)!!
- if (currentAttribute != null) {
- replaceAttribute(
- currentAttribute,
- ngsiLdAttribute,
+
+ if (currentAttribute == null) {
+ addAttribute(
+ entityUri,
+ ngsiLdAttribute.name,
attributeMetadata,
createdAt,
attributePayload,
@@ -682,24 +675,34 @@ class EntityAttributeService(
UpdateAttributeResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED,
- null
+ UpdateOperationResult.APPENDED
)
}.bind()
- } else {
- addAttribute(
+ } else if (hasNgsiLdNullValue(attributePayload, currentAttribute.attributeType)) {
+ deleteAttribute(
entityUri,
ngsiLdAttribute.name,
- attributeMetadata,
+ ngsiLdAttributeInstance.datasetId,
+ false,
+ createdAt
+ ).map {
+ UpdateAttributeResult(
+ ngsiLdAttribute.name,
+ ngsiLdAttributeInstance.datasetId,
+ UpdateOperationResult.DELETED
+ )
+ }.bind()
+ } else {
+ partialUpdateAttribute(
+ entityUri,
+ Pair(ngsiLdAttribute.name, listOf(attributePayload)),
createdAt,
- attributePayload,
sub
).map {
UpdateAttributeResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ UpdateOperationResult.REPLACED
)
}.bind()
}
@@ -715,17 +718,33 @@ class EntityAttributeService(
): Either = either {
val attributeName = expandedAttribute.first
val attributeValues = expandedAttribute.second[0]
- logger.debug(
- "Updating attribute {} of entity {} with values: {}",
- attributeName,
- entityId,
- attributeValues
- )
+ logger.debug("Partial updating attribute {} in entity {}", attributeName, entityId)
val datasetId = attributeValues.getDatasetId()
- val exists = hasAttribute(entityId, attributeName, datasetId).bind()
+ val currentAttribute = getForEntityAndAttribute(entityId, attributeName, datasetId).fold({ null }, { it })
val updateAttributeResult =
- if (exists) {
+ if (currentAttribute == null) {
+ UpdateAttributeResult(
+ attributeName,
+ datasetId,
+ UpdateOperationResult.FAILED,
+ "Unknown attribute $attributeName with datasetId $datasetId in entity $entityId"
+ )
+ } else if (hasNgsiLdNullValue(attributeValues, currentAttribute.attributeType)) {
+ deleteAttribute(
+ entityId,
+ attributeName,
+ datasetId,
+ false,
+ modifiedAt
+ ).map {
+ UpdateAttributeResult(
+ attributeName,
+ datasetId,
+ UpdateOperationResult.DELETED
+ )
+ }.bind()
+ } else {
// first update payload in temporal entity attribute
val attribute = getForEntityAndAttribute(entityId, attributeName, datasetId).bind()
attributeValues[JSONLD_TYPE]?.let {
@@ -752,15 +771,7 @@ class EntityAttributeService(
UpdateAttributeResult(
attributeName,
datasetId,
- UpdateOperationResult.UPDATED,
- null
- )
- } else {
- UpdateAttributeResult(
- attributeName,
- datasetId,
- UpdateOperationResult.FAILED,
- "Unknown attribute $attributeName with datasetId $datasetId in entity $entityId"
+ UpdateOperationResult.UPDATED
)
}
@@ -802,7 +813,7 @@ class EntityAttributeService(
).bind()
} else {
logger.debug("Adding instance to attribute {} to entity {}", currentAttribute.attributeName, entityUri)
- attributeInstanceService.addAttributeInstance(
+ attributeInstanceService.addObservedAttributeInstance(
currentAttribute.id,
attributeMetadata,
expandedAttributes[currentAttribute.attributeName]!!.first()
@@ -831,7 +842,7 @@ class EntityAttributeService(
ngsiLdAttributeInstance.datasetId
)!!
- if (currentAttribute == null) {
+ if (currentAttribute == null)
addAttribute(
entityUri,
ngsiLdAttribute.name,
@@ -843,11 +854,24 @@ class EntityAttributeService(
UpdateAttributeResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.APPENDED,
- null
+ UpdateOperationResult.APPENDED
)
}.bind()
- } else {
+ else if (hasNgsiLdNullValue(attributePayload, currentAttribute.attributeType))
+ deleteAttribute(
+ entityUri,
+ ngsiLdAttribute.name,
+ ngsiLdAttributeInstance.datasetId,
+ false,
+ createdAt
+ ).map {
+ UpdateAttributeResult(
+ ngsiLdAttribute.name,
+ ngsiLdAttributeInstance.datasetId,
+ UpdateOperationResult.DELETED
+ )
+ }.bind()
+ else
mergeAttribute(
currentAttribute,
ngsiLdAttribute.name,
@@ -860,11 +884,9 @@ class EntityAttributeService(
UpdateAttributeResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.UPDATED,
- null
+ UpdateOperationResult.UPDATED
)
}.bind()
- }
}
}.fold({ it.left() }, { updateResultFromDetailedResult(it).right() })
@@ -903,8 +925,7 @@ class EntityAttributeService(
UpdateAttributeResult(
ngsiLdAttribute.name,
ngsiLdAttributeInstance.datasetId,
- UpdateOperationResult.REPLACED,
- null
+ UpdateOperationResult.REPLACED
)
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
index 8586746ef..33d424f90 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityEventService.kt
@@ -188,6 +188,20 @@ class EntityEventService(
)
)
+ UpdateOperationResult.DELETED ->
+ publishEntityEvent(
+ AttributeDeleteEvent(
+ sub,
+ tenantName,
+ entityId,
+ entityTypesAndPayload.first,
+ serializedAttribute.first,
+ updatedDetails.datasetId,
+ serializedAttribute.second,
+ emptyList()
+ )
+ )
+
else ->
logger.warn(
"Received an unexpected result (${updatedDetails.updateOperationResult} " +
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt
index 7c756b569..d97eaadae 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt
@@ -18,14 +18,12 @@ import com.egm.stellio.search.entity.model.EntitiesQueryFromPost
import com.egm.stellio.search.entity.model.Entity
import com.egm.stellio.search.entity.util.rowToEntity
import com.egm.stellio.shared.model.APIException
-import com.egm.stellio.shared.model.AlreadyExistsException
import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.model.ResourceNotFoundException
import com.egm.stellio.shared.util.Sub
import com.egm.stellio.shared.util.buildQQuery
import com.egm.stellio.shared.util.buildScopeQQuery
import com.egm.stellio.shared.util.buildTypeQuery
-import com.egm.stellio.shared.util.entityAlreadyExistsMessage
import com.egm.stellio.shared.util.entityNotFoundMessage
import org.springframework.r2dbc.core.DatabaseClient
import org.springframework.stereotype.Service
@@ -73,6 +71,13 @@ class EntityQueryService(
suspend fun queryEntities(
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
+ ): List =
+ queryEntities(entitiesQuery, true, accessRightFilter)
+
+ suspend fun queryEntities(
+ entitiesQuery: EntitiesQuery,
+ excludedDeleted: Boolean = true,
+ accessRightFilter: () -> String?
): List {
val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter)
@@ -81,8 +86,10 @@ class EntityQueryService(
SELECT DISTINCT(entity_payload.entity_id)
FROM entity_payload
LEFT JOIN temporal_entity_attribute tea
- ON tea.entity_id = entity_payload.entity_id
+ ON tea.entity_id = entity_payload.entity_id
+ ${if (excludedDeleted) " AND tea.deleted_at is null " else ""}
WHERE $filterQuery
+ ${if (excludedDeleted) " AND entity_payload.deleted_at is null " else ""}
ORDER BY entity_id
LIMIT :limit
OFFSET :offset
@@ -98,6 +105,13 @@ class EntityQueryService(
suspend fun queryEntitiesCount(
entitiesQuery: EntitiesQuery,
accessRightFilter: () -> String?
+ ): Either =
+ queryEntitiesCount(entitiesQuery, true, accessRightFilter)
+
+ suspend fun queryEntitiesCount(
+ entitiesQuery: EntitiesQuery,
+ excludedDeleted: Boolean = true,
+ accessRightFilter: () -> String?
): Either {
val filterQuery = buildFullEntitiesFilter(entitiesQuery, accessRightFilter)
@@ -107,7 +121,9 @@ class EntityQueryService(
FROM entity_payload
LEFT JOIN temporal_entity_attribute tea
ON tea.entity_id = entity_payload.entity_id
+ ${if (excludedDeleted) " AND tea.deleted_at is null " else ""}
WHERE $filterQuery
+ ${if (excludedDeleted) " AND entity_payload.deleted_at is null " else ""}
""".trimIndent()
return databaseClient
@@ -232,10 +248,7 @@ class EntityQueryService(
.bind("entities_ids", entitiesIds)
.allToMappedList { it.rowToEntity() }
- suspend fun checkEntityExistence(
- entityId: URI,
- inverse: Boolean = false
- ): Either {
+ suspend fun checkEntityExistence(entityId: URI, allowDeleted: Boolean = false): Either {
val selectQuery =
"""
select
@@ -243,6 +256,7 @@ class EntityQueryService(
select 1
from entity_payload
where entity_id = :entity_id
+ ${if (!allowDeleted) " and deleted_at is null " else ""}
) as entityExists;
""".trimIndent()
@@ -251,15 +265,32 @@ class EntityQueryService(
.bind("entity_id", entityId)
.oneToResult { it["entityExists"] as Boolean }
.flatMap {
- if (it && !inverse || !it && inverse)
+ if (it)
Unit.right()
- else if (it)
- AlreadyExistsException(entityAlreadyExistsMessage(entityId.toString())).left()
else
ResourceNotFoundException(entityNotFoundMessage(entityId.toString())).left()
}
}
+ /**
+ * Used for checks before creating a (temporal) entity. Allows to know if the entity does not exist,
+ * or, if it exists, whether it is currently deleted (in which case, it may be possible to create it again
+ * if authorized)
+ */
+ suspend fun isMarkedAsDeleted(entityId: URI): Either {
+ val selectQuery =
+ """
+ select entity_id, deleted_at
+ from entity_payload
+ where entity_id = :entity_id
+ """.trimIndent()
+
+ return databaseClient
+ .sql(selectQuery)
+ .bind("entity_id", entityId)
+ .oneToResult { it["deleted_at"] != null }
+ }
+
suspend fun filterExistingEntitiesAsIds(entitiesIds: List): List {
if (entitiesIds.isEmpty()) {
return emptyList()
@@ -270,6 +301,7 @@ class EntityQueryService(
select entity_id
from entity_payload
where entity_id in (:entities_ids)
+ and deleted_at is null
""".trimIndent()
return databaseClient
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt
index 800d741a5..cac3a7d27 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityService.kt
@@ -1,6 +1,9 @@
package com.egm.stellio.search.entity.service
import arrow.core.Either
+import arrow.core.Either.Left
+import arrow.core.Either.Right
+import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import arrow.core.toOption
@@ -24,7 +27,9 @@ import com.egm.stellio.search.entity.model.updateResultFromDetailedResult
import com.egm.stellio.search.entity.util.prepareAttributes
import com.egm.stellio.search.entity.util.rowToEntity
import com.egm.stellio.search.scope.ScopeService
+import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty
import com.egm.stellio.shared.model.APIException
+import com.egm.stellio.shared.model.AlreadyExistsException
import com.egm.stellio.shared.model.ExpandedAttribute
import com.egm.stellio.shared.model.ExpandedAttributeInstances
import com.egm.stellio.shared.model.ExpandedAttributes
@@ -35,10 +40,12 @@ import com.egm.stellio.shared.model.addSysAttrs
import com.egm.stellio.shared.model.toExpandedAttributes
import com.egm.stellio.shared.model.toNgsiLdAttributes
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY
import com.egm.stellio.shared.util.JsonUtils.serializeObject
import com.egm.stellio.shared.util.Sub
+import com.egm.stellio.shared.util.entityAlreadyExistsMessage
import com.egm.stellio.shared.util.getSpecificAccessPolicy
import com.egm.stellio.shared.util.ngsiLdDateTime
import io.r2dbc.postgresql.codec.Json
@@ -48,7 +55,6 @@ import org.springframework.r2dbc.core.bind
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.URI
-import java.time.ZoneOffset
import java.time.ZonedDateTime
@Service
@@ -68,14 +74,23 @@ class EntityService(
expandedEntity: ExpandedEntity,
sub: Sub? = null
): Either = either {
- authorizationService.userCanCreateEntities(sub.toOption()).bind()
- entityQueryService.checkEntityExistence(ngsiLdEntity.id, true).bind()
+ entityQueryService.isMarkedAsDeleted(ngsiLdEntity.id).let {
+ when (it) {
+ is Left -> authorizationService.userCanCreateEntities(sub.toOption()).bind()
+ is Right ->
+ if (!it.value)
+ AlreadyExistsException(entityAlreadyExistsMessage(ngsiLdEntity.id.toString())).left().bind()
+ else
+ authorizationService.userCanAdminEntity(ngsiLdEntity.id, sub.toOption()).bind()
+ }
+ }
val createdAt = ngsiLdDateTime()
val attributesMetadata = ngsiLdEntity.prepareAttributes().bind()
logger.debug("Creating entity {}", ngsiLdEntity.id)
- createEntityPayload(ngsiLdEntity, expandedEntity, createdAt, sub).bind()
+ createEntityPayload(ngsiLdEntity, expandedEntity, createdAt).bind()
+ scopeService.createHistory(ngsiLdEntity, createdAt, sub).bind()
entityAttributeService.createAttributes(
ngsiLdEntity,
expandedEntity,
@@ -96,14 +111,20 @@ class EntityService(
suspend fun createEntityPayload(
ngsiLdEntity: NgsiLdEntity,
expandedEntity: ExpandedEntity,
- createdAt: ZonedDateTime,
- sub: Sub? = null
+ createdAt: ZonedDateTime
): Either = either {
val specificAccessPolicy = ngsiLdEntity.getSpecificAccessPolicy()?.bind()
databaseClient.sql(
"""
INSERT INTO entity_payload (entity_id, types, scopes, created_at, payload, specific_access_policy)
VALUES (:entity_id, :types, :scopes, :created_at, :payload, :specific_access_policy)
+ ON CONFLICT (entity_id)
+ DO UPDATE SET types = :types,
+ scopes = :scopes,
+ modified_at = :created_at,
+ deleted_at = null,
+ payload = :payload,
+ specific_access_policy = :specific_access_policy
""".trimIndent()
)
.bind("entity_id", ngsiLdEntity.id)
@@ -113,9 +134,6 @@ class EntityService(
.bind("payload", Json.of(serializeObject(expandedEntity.populateCreationTimeDate(createdAt).members)))
.bind("specific_access_policy", specificAccessPolicy?.toString())
.execute()
- .map {
- scopeService.createHistory(ngsiLdEntity, createdAt, sub)
- }
}
@Transactional
@@ -129,7 +147,10 @@ class EntityService(
authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
val (coreAttrs, otherAttrs) =
- expandedAttributes.toList().partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) }
+ expandedAttributes.toList()
+ // remove @id if it is present (optional as per 5.4)
+ .filter { it.first != JSONLD_ID }
+ .partition { JSONLD_EXPANDED_ENTITY_SPECIFIC_MEMBERS.contains(it.first) }
val mergedAt = ngsiLdDateTime()
logger.debug("Merging entity {}", entityId)
@@ -173,13 +194,14 @@ class EntityService(
entityQueryService.checkEntityExistence(entityId).bind()
authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
- val replacedAt = ngsiLdDateTime()
val attributesMetadata = ngsiLdEntity.prepareAttributes().bind()
logger.debug("Replacing entity {}", ngsiLdEntity.id)
- entityAttributeService.deleteAttributes(entityId)
+ entityAttributeService.deleteAttributes(entityId, ngsiLdDateTime()).bind()
- replaceEntityPayload(ngsiLdEntity, expandedEntity, replacedAt, sub).bind()
+ val replacedAt = ngsiLdDateTime()
+ replaceEntityPayload(ngsiLdEntity, expandedEntity, replacedAt).bind()
+ scopeService.replace(ngsiLdEntity, replacedAt, sub).bind()
entityAttributeService.createAttributes(
ngsiLdEntity,
expandedEntity,
@@ -199,8 +221,7 @@ class EntityService(
suspend fun replaceEntityPayload(
ngsiLdEntity: NgsiLdEntity,
expandedEntity: ExpandedEntity,
- replacedAt: ZonedDateTime,
- sub: Sub? = null
+ replacedAt: ZonedDateTime
): Either = either {
val specificAccessPolicy = ngsiLdEntity.getSpecificAccessPolicy()?.bind()
val createdAt = retrieveCreatedAt(ngsiLdEntity.id).bind()
@@ -225,9 +246,6 @@ class EntityService(
.bind("payload", Json.of(serializedPayload))
.bind("specific_access_policy", specificAccessPolicy?.toString())
.execute()
- .map {
- scopeService.replaceHistoryEntry(ngsiLdEntity, createdAt, sub)
- }
}
private suspend fun retrieveCreatedAt(entityId: URI): Either =
@@ -439,7 +457,7 @@ class EntityService(
expandedAttributes: ExpandedAttributes,
sub: Sub? = null
): Either = either {
- val createdAt = ZonedDateTime.now(ZoneOffset.UTC)
+ val createdAt = ngsiLdDateTime()
expandedAttributes.forEach { (attributeName, expandedAttributeInstances) ->
expandedAttributeInstances.forEach { expandedAttributeInstance ->
val jsonLdAttribute = mapOf(attributeName to listOf(expandedAttributeInstance))
@@ -545,35 +563,57 @@ class EntityService(
}
@Transactional
- suspend fun upsertEntityPayload(entityId: URI, payload: String): Either =
- databaseClient.sql(
+ suspend fun deleteEntity(entityId: URI, sub: Sub? = null): Either = either {
+ entityQueryService.checkEntityExistence(entityId).bind()
+ authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind()
+
+ val deletedAt = ngsiLdDateTime()
+ val entity = deleteEntityPayload(entityId, deletedAt).bind()
+ entityAttributeService.deleteAttributes(entityId, deletedAt).bind()
+ scopeService.addHistoryEntry(entityId, emptyList(), TemporalProperty.DELETED_AT, deletedAt, sub).bind()
+
+ entityEventService.publishEntityDeleteEvent(sub, entity)
+ }
+
+ @Transactional
+ suspend fun deleteEntityPayload(entityId: URI, deletedAt: ZonedDateTime): Either = either {
+ val expandedDeletedEntity = Entity.toExpandedDeletedEntity(entityId, deletedAt)
+ val entity = databaseClient.sql(
"""
- INSERT INTO entity_payload (entity_id, payload)
- VALUES (:entity_id, :payload)
- ON CONFLICT (entity_id)
- DO UPDATE SET payload = :payload
+ UPDATE entity_payload
+ SET deleted_at = :deleted_at,
+ payload = :payload,
+ scopes = null,
+ specific_access_policy = null,
+ types = '{}'
+ WHERE entity_id = :entity_id
+ RETURNING *
""".trimIndent()
)
- .bind("payload", Json.of(payload))
.bind("entity_id", entityId)
- .execute()
+ .bind("deleted_at", deletedAt)
+ .bind("payload", Json.of(serializeObject(expandedDeletedEntity.members)))
+ .oneToResult {
+ it.rowToEntity()
+ }
+ .bind()
+ entity
+ }
@Transactional
- suspend fun deleteEntity(entityId: URI, sub: Sub? = null): Either = either {
- entityQueryService.checkEntityExistence(entityId).bind()
+ suspend fun permanentlyDeleteEntity(entityId: URI, sub: Sub? = null): Either = either {
+ entityQueryService.checkEntityExistence(entityId, true).bind()
authorizationService.userCanAdminEntity(entityId, sub.toOption()).bind()
- val entity = deleteEntityPayload(entityId).bind()
-
- entityAttributeService.deleteAttributes(entityId).bind()
- scopeService.deleteHistory(entityId).bind()
+ val entity = permanentyDeleteEntityPayload(entityId).bind()
+ entityAttributeService.permanentlyDeleteAttributes(entityId).bind()
authorizationService.removeRightsOnEntity(entityId).bind()
entityEventService.publishEntityDeleteEvent(sub, entity)
}
@Transactional
- suspend fun deleteEntityPayload(entityId: URI): Either = either {
+ suspend fun permanentyDeleteEntityPayload(entityId: URI): Either = either {
val entity = databaseClient.sql(
"""
DELETE FROM entity_payload
@@ -612,7 +652,8 @@ class EntityService(
entityId,
attributeName,
datasetId,
- deleteAll
+ deleteAll,
+ ngsiLdDateTime()
).bind()
}
updateState(
@@ -629,4 +670,36 @@ class EntityService(
deleteAll
)
}
+
+ @Transactional
+ suspend fun permanentlyDeleteAttribute(
+ entityId: URI,
+ attributeName: ExpandedTerm,
+ datasetId: URI?,
+ deleteAll: Boolean = false,
+ sub: Sub? = null
+ ): Either = either {
+ authorizationService.userCanUpdateEntity(entityId, sub.toOption()).bind()
+
+ if (attributeName == NGSILD_SCOPE_PROPERTY) {
+ scopeService.permanentlyDelete(entityId).bind()
+ } else {
+ entityAttributeService.checkEntityAndAttributeExistence(
+ entityId,
+ attributeName,
+ datasetId
+ ).bind()
+ entityAttributeService.permanentlyDeleteAttribute(
+ entityId,
+ attributeName,
+ datasetId,
+ deleteAll
+ ).bind()
+ }
+ updateState(
+ entityId,
+ ngsiLdDateTime(),
+ entityAttributeService.getForEntity(entityId, emptySet(), emptySet())
+ ).bind()
+ }
}
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt
index 32d2c2c35..af8e883d6 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/AttributeUtils.kt
@@ -7,6 +7,7 @@ import arrow.core.right
import com.egm.stellio.search.common.util.deserializeAsMap
import com.egm.stellio.search.common.util.valueToDoubleOrNull
import com.egm.stellio.search.entity.model.Attribute
+import com.egm.stellio.search.entity.model.Attribute.AttributeType
import com.egm.stellio.search.entity.model.AttributeMetadata
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.BadRequestDataException
@@ -20,12 +21,17 @@ import com.egm.stellio.shared.model.NgsiLdPropertyInstance
import com.egm.stellio.shared.model.NgsiLdRelationshipInstance
import com.egm.stellio.shared.model.NgsiLdVocabPropertyInstance
import com.egm.stellio.shared.model.WKTCoordinates
+import com.egm.stellio.shared.model.getMemberValue
import com.egm.stellio.shared.model.getPropertyValue
+import com.egm.stellio.shared.model.getRelationshipId
import com.egm.stellio.shared.util.JsonLdUtils
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NULL
import com.egm.stellio.shared.util.JsonUtils
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import com.savvasdalkitsis.jsonmerger.JsonMerger
import io.r2dbc.postgresql.codec.Json
+import java.net.URI
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZonedDateTime
@@ -47,35 +53,35 @@ fun NgsiLdAttributeInstance.toAttributeMetadata(): Either
guessPropertyValueType(this).let {
- Triple(Attribute.AttributeType.Property, it.first, it.second)
+ Triple(AttributeType.Property, it.first, it.second)
}
is NgsiLdRelationshipInstance ->
Triple(
- Attribute.AttributeType.Relationship,
+ AttributeType.Relationship,
Attribute.AttributeValueType.URI,
Triple(this.objectId.toString(), null, null)
)
is NgsiLdGeoPropertyInstance ->
Triple(
- Attribute.AttributeType.GeoProperty,
+ AttributeType.GeoProperty,
Attribute.AttributeValueType.GEOMETRY,
Triple(null, null, this.coordinates)
)
is NgsiLdJsonPropertyInstance ->
Triple(
- Attribute.AttributeType.JsonProperty,
+ AttributeType.JsonProperty,
Attribute.AttributeValueType.JSON,
Triple(JsonUtils.serializeObject(this.json), null, null)
)
is NgsiLdLanguagePropertyInstance ->
Triple(
- Attribute.AttributeType.LanguageProperty,
+ AttributeType.LanguageProperty,
Attribute.AttributeValueType.ARRAY,
Triple(JsonUtils.serializeObject(this.languageMap), null, null)
)
is NgsiLdVocabPropertyInstance ->
Triple(
- Attribute.AttributeType.VocabProperty,
+ AttributeType.VocabProperty,
Attribute.AttributeValueType.ARRAY,
Triple(JsonUtils.serializeObject(this.vocab), null, null)
)
@@ -97,17 +103,17 @@ fun NgsiLdAttributeInstance.toAttributeMetadata(): Either
+ AttributeType.Property ->
guessPropertyValueType(expandedAttributeInstance.getPropertyValue()!!).first
- Attribute.AttributeType.Relationship -> Attribute.AttributeValueType.URI
- Attribute.AttributeType.GeoProperty -> Attribute.AttributeValueType.GEOMETRY
- Attribute.AttributeType.JsonProperty -> Attribute.AttributeValueType.JSON
- Attribute.AttributeType.LanguageProperty -> Attribute.AttributeValueType.ARRAY
- Attribute.AttributeType.VocabProperty -> Attribute.AttributeValueType.ARRAY
+ AttributeType.Relationship -> Attribute.AttributeValueType.URI
+ AttributeType.GeoProperty -> Attribute.AttributeValueType.GEOMETRY
+ AttributeType.JsonProperty -> Attribute.AttributeValueType.JSON
+ AttributeType.LanguageProperty -> Attribute.AttributeValueType.ARRAY
+ AttributeType.VocabProperty -> Attribute.AttributeValueType.ARRAY
}
fun guessPropertyValueType(
@@ -131,6 +137,21 @@ fun guessPropertyValueType(
else -> Pair(Attribute.AttributeValueType.STRING, Triple(value.toString(), null, null))
}
+/**
+ * Returns whether the expanded attribute instance holds a NGSI-LD Null value
+ */
+fun hasNgsiLdNullValue(
+ expandedAttributeInstance: ExpandedAttributeInstance,
+ attributeType: AttributeType
+): Boolean =
+ if (attributeType == AttributeType.Relationship) {
+ val value = expandedAttributeInstance.getRelationshipId()
+ value is URI && value.toString() == NGSILD_NULL
+ } else {
+ val value = expandedAttributeInstance.getMemberValue(attributeType.toExpandedValueMember())
+ value is String && value == NGSILD_NULL
+ }
+
fun Json.toExpandedAttributeInstance(): ExpandedAttributeInstance =
this.deserializeAsMap() as ExpandedAttributeInstance
@@ -169,7 +190,7 @@ fun mergePatch(
).deserializeAsMap()
)
}
- } else if (listOf(JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE).contains(attrName)) {
+ } else if (listOf(NGSILD_LANGUAGEPROPERTY_VALUE).contains(attrName)) {
val sourceLangEntries = source[attrName] as List