diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntitiesQuery.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/EntitiesQuery.kt index 7818f984fc..46ff3343ab 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntitiesQuery.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/EntitiesQuery.kt @@ -14,8 +14,6 @@ data class EntitiesQuery( val scopeQ: String? = null, val paginationQuery: PaginationQuery, val attrs: Set = emptySet(), - val includeSysAttrs: Boolean = false, - val useSimplifiedRepresentation: Boolean = false, val geoQuery: GeoQuery? = null, val context: String ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityPayload.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityPayload.kt index af46f645aa..b36ffc8185 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityPayload.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityPayload.kt @@ -10,7 +10,9 @@ 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_CREATED_AT_PROPERTY +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_TERM import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_MODIFIED_AT_TERM import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_PROPERTY import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SCOPE_TERM import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedProperty @@ -33,7 +35,6 @@ data class EntityPayload( val specificAccessPolicy: SpecificAccessPolicy? = null ) { fun serializeProperties( - withSysAttrs: Boolean, withCompactTerms: Boolean = false, contexts: List = emptyList() ): Map { @@ -56,6 +57,11 @@ data class EntityPayload( JSONLD_VALUE_TERM to this ) } + + resultEntity[NGSILD_CREATED_AT_TERM] = createdAt + modifiedAt?.run { + resultEntity[NGSILD_MODIFIED_AT_TERM] = this + } } else { resultEntity[JSONLD_ID] = entityId.toString() resultEntity[JSONLD_TYPE] = types @@ -67,14 +73,13 @@ data class EntityPayload( specificAccessPolicy?.run { resultEntity[AuthContextModel.AUTH_PROP_SAP] = buildExpandedProperty(this) } - if (withSysAttrs) { - resultEntity[NGSILD_CREATED_AT_PROPERTY] = buildNonReifiedDateTime(createdAt) - modifiedAt?.run { - resultEntity[NGSILD_MODIFIED_AT_PROPERTY] = buildNonReifiedDateTime(this) - } + resultEntity[NGSILD_CREATED_AT_PROPERTY] = buildNonReifiedDateTime(createdAt) + modifiedAt?.run { + resultEntity[NGSILD_MODIFIED_AT_PROPERTY] = buildNonReifiedDateTime(this) } } + return resultEntity } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt index ad2d392534..f0d046904b 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt @@ -650,7 +650,7 @@ class EntityPayloadService( temporalEntityAttributes: List, entityPayload: EntityPayload ): Map { - val entityCoreAttributes = entityPayload.serializeProperties(withSysAttrs = true) + val entityCoreAttributes = entityPayload.serializeProperties() val expandedAttributes = temporalEntityAttributes .groupBy { tea -> tea.attributeName diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/EntitiesQueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/util/EntitiesQueryUtils.kt index 5a28e2e95d..b0d2d10e32 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/EntitiesQueryUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/util/EntitiesQueryUtils.kt @@ -28,10 +28,6 @@ fun composeEntitiesQuery( val q = requestParams.getFirst(QUERY_PARAM_Q)?.decode() val scopeQ = requestParams.getFirst(QUERY_PARAM_SCOPEQ) val attrs = parseAndExpandRequestParameter(requestParams.getFirst(QUERY_PARAM_ATTRS), contextLink) - val includeSysAttrs = requestParams.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) - .contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) - val useSimplifiedRepresentation = requestParams.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) - .contains(QUERY_PARAM_OPTIONS_KEYVALUES_VALUE) val paginationQuery = parsePaginationParameters( requestParams, defaultPagination.limitDefault, @@ -48,8 +44,6 @@ fun composeEntitiesQuery( scopeQ = scopeQ, paginationQuery = paginationQuery, attrs = attrs, - includeSysAttrs = includeSysAttrs, - useSimplifiedRepresentation = useSimplifiedRepresentation, geoQuery = geoQuery, context = contextLink ) @@ -99,10 +93,6 @@ fun composeEntitiesQueryFromPostRequest( parseGeoQueryParameters(geoQueryElements, contextLink).bind() } else null - val includeSysAttrs = requestParams.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) - .contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) - val useSimplifiedRepresentation = requestParams.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) - .contains(QUERY_PARAM_OPTIONS_KEYVALUES_VALUE) val paginationQuery = parsePaginationParameters( requestParams, defaultPagination.limitDefault, @@ -117,8 +107,6 @@ fun composeEntitiesQueryFromPostRequest( scopeQ = query.scopeQ, paginationQuery = paginationQuery, attrs = attrs, - includeSysAttrs = includeSysAttrs, - useSimplifiedRepresentation = useSimplifiedRepresentation, geoQuery = geoQuery, context = contextLink ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalEntityBuilder.kt b/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalEntityBuilder.kt index ab81a8ab19..524011dc02 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalEntityBuilder.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/util/TemporalEntityBuilder.kt @@ -43,7 +43,6 @@ object TemporalEntityBuilder { ) return entityTemporalResult.entityPayload.serializeProperties( - withSysAttrs = temporalEntitiesQuery.entitiesQuery.includeSysAttrs, withCompactTerms = true, contexts ).plus(temporalAttributes) @@ -222,3 +221,15 @@ object TemporalEntityBuilder { } } } + +/** + * A specific application of sysAttrs option to temporal entities, that only applies it to the entity and not to the + * attributes. Currently, this is not clear what should be done for attributes who already convey specifically asked + * temporal information. + */ +fun CompactedJsonLdEntity.applySysAttrs(includeSysAttrs: Boolean) = + this.let { + if (!includeSysAttrs) + it.minus(JsonLdUtils.NGSILD_SYSATTRS_TERMS) + else it + } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt index ef912a967e..59b125b510 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt @@ -23,7 +23,7 @@ class AttributeHandler( @RequestParam details: Optional ): ResponseEntity<*> = either { val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val detailedRepresentation = details.orElse(false) val availableAttribute: Any = if (detailedRepresentation) @@ -46,7 +46,7 @@ class AttributeHandler( @PathVariable attrId: String ): ResponseEntity<*> = either { val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val expandedAttribute = JsonLdUtils.expandJsonLdTerm(attrId.decode(), contextLink) val attributeTypeInfo = diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt index 2e4939a450..3e97bb9e66 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt @@ -46,7 +46,7 @@ class EntityAccessControlHandler( val sub = getSubFromSecurityContext() val contextLink = getAuthzContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val entitiesQuery = composeEntitiesQuery( applicationProperties.pagination, @@ -71,14 +71,16 @@ class EntityAccessControlHandler( val compactedEntities = JsonLdUtils.compactEntities( countAndAuthorizedEntities.second, - entitiesQuery.useSimplifiedRepresentation, - entitiesQuery.includeSysAttrs, contextLink, mediaType ) + val ngsiLdDataRepresentation = parseRepresentations( + params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()), + mediaType + ) buildQueryResponse( - compactedEntities, + compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), countAndAuthorizedEntities.first, "/ngsi-ld/v1/entityAccessControl/entities", entitiesQuery.paginationQuery, @@ -99,7 +101,7 @@ class EntityAccessControlHandler( val sub = getSubFromSecurityContext() val contextLink = getAuthzContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val entitiesQuery = composeEntitiesQuery( applicationProperties.pagination, params, @@ -120,14 +122,16 @@ class EntityAccessControlHandler( val compactedEntities = JsonLdUtils.compactEntities( countAndGroupEntities.second, - entitiesQuery.useSimplifiedRepresentation, - entitiesQuery.includeSysAttrs, contextLink, mediaType ) + val ngsiLdDataRepresentation = parseRepresentations( + params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()), + mediaType + ) buildQueryResponse( - compactedEntities, + compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), countAndGroupEntities.first, "/ngsi-ld/v1/entityAccessControl/groups", entitiesQuery.paginationQuery, @@ -150,7 +154,7 @@ class EntityAccessControlHandler( authorizationService.userIsAdmin(sub).bind() val contextLink = getAuthzContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val entitiesQuery = composeEntitiesQuery( applicationProperties.pagination, params, @@ -170,14 +174,16 @@ class EntityAccessControlHandler( val compactedEntities = JsonLdUtils.compactEntities( countAndUserEntities.second, - entitiesQuery.useSimplifiedRepresentation, - entitiesQuery.includeSysAttrs, contextLink, mediaType ) + val ngsiLdDataRepresentation = parseRepresentations( + params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()), + mediaType + ) buildQueryResponse( - compactedEntities, + compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), countAndUserEntities.first, "/ngsi-ld/v1/entityAccessControl/users", entitiesQuery.paginationQuery, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt index a09e2a0023..3adeed884d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt @@ -22,7 +22,7 @@ import com.egm.stellio.shared.util.JsonUtils.serializeObject import com.egm.stellio.shared.web.BaseHandler import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus -import org.springframework.http.MediaType +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE import org.springframework.http.ResponseEntity import org.springframework.util.MultiValueMap import org.springframework.web.bind.annotation.* @@ -43,7 +43,7 @@ class EntityHandler( /** * Implements 6.4.3.1 - Create Entity */ - @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) + @PostMapping(consumes = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun create( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono @@ -81,7 +81,7 @@ class EntityHandler( /** * Implements 6.5.3.4 - Merge Entity */ - @PatchMapping("/{entityId}", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) + @PatchMapping("/{entityId}", consumes = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun merge( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @@ -135,7 +135,7 @@ class EntityHandler( /** * Implements 6.5.3.3 - Replace Entity */ - @PutMapping("/{entityId}", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) + @PutMapping("/{entityId}", consumes = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun replace( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @@ -181,12 +181,12 @@ class EntityHandler( /** * Implements 6.4.3.2 - Query Entities */ - @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) + @GetMapping(produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, @RequestParam params: MultiValueMap ): ResponseEntity<*> = either { - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val sub = getSubFromSecurityContext() val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() @@ -204,14 +204,16 @@ class EntityHandler( val compactedEntities = JsonLdUtils.compactEntities( filteredEntities, - entitiesQuery.useSimplifiedRepresentation, - entitiesQuery.includeSysAttrs, contextLink, mediaType ) + val ngsiLdDataRepresentation = parseRepresentations( + params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()), + mediaType + ) buildQueryResponse( - compactedEntities, + compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), countAndEntities.second, "/ngsi-ld/v1/entities", entitiesQuery.paginationQuery, @@ -227,13 +229,13 @@ class EntityHandler( /** * Implements 6.5.3.1 - Retrieve Entity */ - @GetMapping("/{entityId}", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) + @GetMapping("/{entityId}", produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE]) suspend fun getByURI( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @RequestParam params: MultiValueMap ): ResponseEntity<*> = either { - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val sub = getSubFromSecurityContext() val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() @@ -255,16 +257,14 @@ class EntityHandler( JsonLdUtils.filterJsonLdEntityOnAttributes(jsonLdEntity, queryParams.attrs), jsonLdEntity.contexts ) - val compactedEntity = JsonLdUtils.compact(filteredJsonLdEntity, contextLink, mediaType).toMutableMap() - - prepareGetSuccessResponse(mediaType, contextLink).body( - serializeObject( - compactedEntity.toFinalRepresentation( - queryParams.includeSysAttrs, - queryParams.useSimplifiedRepresentation - ) - ) + val compactedEntity = JsonLdUtils.compactEntity(filteredJsonLdEntity, contextLink, mediaType).toMutableMap() + + val ngsiLdDataRepresentation = parseRepresentations( + params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()), + mediaType ) + prepareGetSuccessResponse(mediaType, contextLink) + .body(serializeObject(compactedEntity.toFinalRepresentation(ngsiLdDataRepresentation))) }.fold( { it.toErrorResponse() }, { it } @@ -302,7 +302,7 @@ class EntityHandler( * Implements 6.6.3.1 - Append Entity Attributes * */ - @PostMapping("/{entityId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) + @PostMapping("/{entityId}/attrs", consumes = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun appendEntityAttributes( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @@ -356,7 +356,7 @@ class EntityHandler( */ @PatchMapping( "/{entityId}/attrs", - consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, JSON_MERGE_PATCH_CONTENT_TYPE] + consumes = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, JSON_MERGE_PATCH_CONTENT_TYPE] ) suspend fun updateEntityAttributes( @RequestHeader httpHeaders: HttpHeaders, @@ -406,7 +406,7 @@ class EntityHandler( */ @PatchMapping( "/{entityId}/attrs/{attrId}", - consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, JSON_MERGE_PATCH_CONTENT_TYPE] + consumes = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, JSON_MERGE_PATCH_CONTENT_TYPE] ) suspend fun partialAttributeUpdate( @RequestHeader httpHeaders: HttpHeaders, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt index a8ce1a87d6..02bcc39dab 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt @@ -260,7 +260,7 @@ class EntityOperationHandler( ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val entitiesQuery = composeEntitiesQueryFromPostRequest( applicationProperties.pagination, @@ -277,14 +277,16 @@ class EntityOperationHandler( val compactedEntities = JsonLdUtils.compactEntities( filteredEntities, - entitiesQuery.useSimplifiedRepresentation, - entitiesQuery.includeSysAttrs, contextLink, mediaType ) + val ngsiLdDataRepresentation = parseRepresentations( + params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()), + mediaType + ) buildQueryResponse( - compactedEntities, + compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), countAndEntities.second, "/ngsi-ld/v1/entities", entitiesQuery.paginationQuery, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityTypeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityTypeHandler.kt index 911c10e4f6..a120d9e3bb 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityTypeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityTypeHandler.kt @@ -25,7 +25,7 @@ class EntityTypeHandler( @RequestParam details: Optional ): ResponseEntity<*> = either { val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val detailedRepresentation = details.orElse(false) val availableEntityTypes: Any = if (detailedRepresentation) @@ -49,7 +49,7 @@ class EntityTypeHandler( @PathVariable type: String ): ResponseEntity<*> = either { val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val expandedType = expandJsonLdTerm(type.decode(), contextLink) val entityTypeInfo = entityTypeService.getEntityTypeInfoByType(expandedType, listOf(contextLink)).bind() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt index 1d7e1288b7..d56a883fc8 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt @@ -3,6 +3,7 @@ package com.egm.stellio.search.web import arrow.core.raise.either import com.egm.stellio.search.authorization.AuthorizationService import com.egm.stellio.search.service.* +import com.egm.stellio.search.util.applySysAttrs import com.egm.stellio.search.util.composeTemporalEntitiesQuery import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.* @@ -137,16 +138,20 @@ class TemporalEntityHandler( ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val temporalEntitiesQuery = composeTemporalEntitiesQuery(applicationProperties.pagination, params, contextLink, true).bind() val accessRightFilter = authorizationService.computeAccessRightFilter(sub) + + val includeSysAttrs = params.contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) val (temporalEntities, total) = queryService.queryTemporalEntities( temporalEntitiesQuery, accessRightFilter - ).bind() + ).bind().let { + Pair(it.first.map { it.applySysAttrs(includeSysAttrs) }, it.second) + } buildQueryResponse( serializeObject(temporalEntities.map { addContextsToEntity(it, listOf(contextLink), mediaType) }), @@ -176,18 +181,20 @@ class TemporalEntityHandler( entityPayloadService.checkEntityExistence(entityId).bind() val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() authorizationService.userCanReadEntity(entityId, sub).bind() val temporalEntitiesQuery = composeTemporalEntitiesQuery(applicationProperties.pagination, requestParams, contextLink).bind() + val includeSysAttrs = requestParams.contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) val temporalEntity = queryService.queryTemporalEntity( entityId, temporalEntitiesQuery, contextLink ).bind() + .applySysAttrs(includeSysAttrs) prepareGetSuccessResponse(mediaType, contextLink) .body(serializeObject(addContextsToEntity(temporalEntity, listOf(contextLink), mediaType))) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt index 714fe46e0c..d0b596ec47 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt @@ -3,6 +3,7 @@ package com.egm.stellio.search.web import arrow.core.raise.either import com.egm.stellio.search.authorization.AuthorizationService import com.egm.stellio.search.service.QueryService +import com.egm.stellio.search.util.applySysAttrs import com.egm.stellio.search.util.composeTemporalEntitiesQueryFromPostRequest import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.util.* @@ -35,7 +36,7 @@ class TemporalEntityOperationsHandler( ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val temporalEntitiesQuery = composeTemporalEntitiesQueryFromPostRequest( @@ -46,10 +47,14 @@ class TemporalEntityOperationsHandler( ).bind() val accessRightFilter = authorizationService.computeAccessRightFilter(sub) + + val includeSysAttrs = params.contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) val (temporalEntities, total) = queryService.queryTemporalEntities( temporalEntitiesQuery, accessRightFilter - ).bind() + ).bind().let { + Pair(it.first.map { it.applySysAttrs(includeSysAttrs) }, it.second) + } buildQueryResponse( serializeObject(temporalEntities.map { addContextsToEntity(it, listOf(contextLink), mediaType) }), diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/model/EntityModelTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/model/EntityModelTests.kt index babbf15fe6..96729ba09a 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/model/EntityModelTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/model/EntityModelTests.kt @@ -21,19 +21,8 @@ class EntityModelTests { ) @Test - fun `it should serialize entityPayload without createdAt and modifiedAt if not specified`() { - val serializedEntity = entityPayload.serializeProperties(false) - assertFalse(serializedEntity.contains(JsonLdUtils.NGSILD_CREATED_AT_PROPERTY)) - assertFalse(serializedEntity.contains(JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY)) - assertFalse(serializedEntity.contains(AuthContextModel.AUTH_PROP_SAP)) - assertEquals(setOf(JsonLdUtils.JSONLD_ID, JsonLdUtils.JSONLD_TYPE), serializedEntity.keys) - assertEquals("urn:ngsi-ld:beehive:01", serializedEntity[JsonLdUtils.JSONLD_ID]) - assertEquals(listOf(BEEHIVE_TYPE), serializedEntity[JsonLdUtils.JSONLD_TYPE]) - } - - @Test - fun `it should serialize entityPayload with createdAt and modifiedAt if specified`() { - val serializedEntity = entityPayload.serializeProperties(true) + fun `it should serialize entityPayload with createdAt and modifiedAt`() { + val serializedEntity = entityPayload.serializeProperties() assertTrue(serializedEntity.contains(JsonLdUtils.NGSILD_CREATED_AT_PROPERTY)) assertTrue(serializedEntity.contains(JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY)) assertFalse(serializedEntity.contains(AuthContextModel.AUTH_PROP_SAP)) @@ -52,7 +41,6 @@ class EntityModelTests { val entityPayloadWithSAP = entityPayload.copy(specificAccessPolicy = AuthContextModel.SpecificAccessPolicy.AUTH_WRITE) val serializedEntity = entityPayloadWithSAP.serializeProperties( - withSysAttrs = false, withCompactTerms = true, contexts = listOf(APIC_COMPOUND_CONTEXT) ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt index 85306a91ef..71d6b7c89c 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt @@ -386,6 +386,7 @@ class QueryServiceTests { { "id": "urn:ngsi-ld:BeeHive:TESTC", "type": "BeeHive", + "createdAt": "$now", "incoming": { "type": "Property", "avg": [] diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityBuilderTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityBuilderTests.kt index 8710953aac..82ccb979e3 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityBuilderTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityBuilderTests.kt @@ -8,6 +8,7 @@ import com.egm.stellio.search.support.buildDefaultQueryParams import com.egm.stellio.search.util.TemporalEntityAttributeInstancesResult import com.egm.stellio.search.util.TemporalEntityBuilder import com.egm.stellio.shared.util.* +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_TERM import com.egm.stellio.shared.util.JsonUtils.serializeObject import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -54,7 +55,8 @@ class TemporalEntityBuilderTests { ) assertJsonPayloadsAreEqual( loadSampleData("expectations/beehive_empty_outgoing.jsonld"), - serializeObject(temporalEntity) + serializeObject(temporalEntity), + setOf(NGSILD_CREATED_AT_TERM) ) } @@ -86,7 +88,7 @@ class TemporalEntityBuilderTests { ), listOf(APIC_COMPOUND_CONTEXT) ) - assertJsonPayloadsAreEqual(expectation, serializeObject(temporalEntity)) + assertJsonPayloadsAreEqual(expectation, serializeObject(temporalEntity), setOf(NGSILD_CREATED_AT_TERM)) } @ParameterizedTest @@ -108,7 +110,7 @@ class TemporalEntityBuilderTests { ), listOf(APIC_COMPOUND_CONTEXT) ) - assertJsonPayloadsAreEqual(expectation, serializeObject(temporalEntity)) + assertJsonPayloadsAreEqual(expectation, serializeObject(temporalEntity), setOf(NGSILD_CREATED_AT_TERM)) } @Test @@ -187,7 +189,8 @@ class TemporalEntityBuilderTests { assertJsonPayloadsAreEqual( loadSampleData("expectations/beehive_aggregated_outgoing.jsonld"), - serializeObject(temporalEntity) + serializeObject(temporalEntity), + setOf(NGSILD_CREATED_AT_TERM) ) } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/util/EntitiesQueryUtilsTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/util/EntitiesQueryUtilsTests.kt index ac6f0d2fa3..d8fc253425 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/util/EntitiesQueryUtilsTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/util/EntitiesQueryUtilsTests.kt @@ -44,21 +44,6 @@ class EntitiesQueryUtilsTests { assertEquals(true, entitiesQuery.paginationQuery.count) assertEquals(1, entitiesQuery.paginationQuery.offset) assertEquals(10, entitiesQuery.paginationQuery.limit) - assertEquals(true, entitiesQuery.useSimplifiedRepresentation) - assertEquals(false, entitiesQuery.includeSysAttrs) - } - - @Test - fun `it should set includeSysAttrs at true if options contains includeSysAttrs query parameters`() = runTest { - val requestParams = LinkedMultiValueMap() - requestParams.add("options", "sysAttrs") - val queryParams = composeEntitiesQuery( - ApplicationProperties.Pagination(30, 100), - requestParams, - NGSILD_CORE_CONTEXT - ).shouldSucceedAndResult() - - assertEquals(true, queryParams.includeSysAttrs) } @Test @@ -91,8 +76,6 @@ class EntitiesQueryUtilsTests { assertEquals(false, entitiesQuery.paginationQuery.count) assertEquals(0, entitiesQuery.paginationQuery.offset) assertEquals(30, entitiesQuery.paginationQuery.limit) - assertEquals(false, entitiesQuery.useSimplifiedRepresentation) - assertEquals(false, entitiesQuery.includeSysAttrs) } private fun gimmeEntitiesQueryParams(): LinkedMultiValueMap { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt index e84ab44b5a..403a545a49 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt @@ -919,7 +919,6 @@ class EntityHandlerTests { EntitiesQuery( typeSelection = "https://uri.etsi.org/ngsi-ld/default-context/Beehive", paginationQuery = PaginationQuery(offset = 0, limit = 30), - includeSysAttrs = true, context = NGSILD_CORE_CONTEXT ), any() diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/APiExceptions.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/APiExceptions.kt index 9816662d26..96b9dc1681 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/APiExceptions.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/APiExceptions.kt @@ -18,6 +18,7 @@ data class AccessDeniedException(override val message: String) : APIException(me data class NotImplementedException(override val message: String) : APIException(message) data class LdContextNotAvailableException(override val message: String) : APIException(message) data class NonexistentTenantException(override val message: String) : APIException(message) +data class NotAcceptableException(override val message: String) : APIException(message) fun Throwable.toAPIException(fallbackMessage: String = ""): APIException = when (this) { diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt index b0edd9c4dc..c8cade7873 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt @@ -80,4 +80,71 @@ data class JsonLdEntity( (members[JSONLD_TYPE] ?: InternalErrorException("Could not extract type from JSON-LD entity")) as List } + + private fun Map.addDateTimeProperty(propertyKey: String, dateTime: ZonedDateTime?): Map = + if (dateTime != null) + this.plus(propertyKey to JsonLdUtils.buildNonReifiedDateTime(dateTime)) + else this +} + +fun CompactedJsonLdEntity.toKeyValues(): Map = + this.mapValues { (_, value) -> simplifyRepresentation(value) } + +private fun simplifyRepresentation(value: Any): Any { + return when (value) { + // entity property value is always a Map + is Map<*, *> -> simplifyValue(value as Map) + is List<*> -> value.map { + when (it) { + is Map<*, *> -> simplifyValue(it as Map) + // we keep @context value as it is (List) + else -> it + } + } + // we keep id and type values as they are (String) + else -> value + } +} + +private fun simplifyValue(value: Map): Any { + return when (value["type"]) { + "Property", "GeoProperty" -> value.getOrDefault("value", value) + "Relationship" -> value.getOrDefault("object", value) + else -> value + } } + +fun CompactedJsonLdEntity.withoutSysAttrs(): Map = + this.filter { + !JsonLdUtils.NGSILD_SYSATTRS_TERMS.contains(it.key) + }.mapValues { + when (it.value) { + is Map<*, *> -> (it.value as Map<*, *>).minus(JsonLdUtils.NGSILD_SYSATTRS_TERMS) + is List<*> -> (it.value as List<*>).map { valueInstance -> + when (valueInstance) { + is Map<*, *> -> valueInstance.minus(JsonLdUtils.NGSILD_SYSATTRS_TERMS) + // we keep @context value as it is (List) + else -> valueInstance + } + } + else -> it.value + } + } + +fun CompactedJsonLdEntity.toFinalRepresentation( + ngsiLdDataRepresentation: NgsiLdDataRepresentation +): CompactedJsonLdEntity = + this.let { + if (!ngsiLdDataRepresentation.includeSysAttrs) it.withoutSysAttrs() + else it + }.let { + if (ngsiLdDataRepresentation.attributeRepresentation == AttributeRepresentation.SIMPLIFIED) it.toKeyValues() + else it + } + +fun List.toFinalRepresentation( + ngsiLdDataRepresentation: NgsiLdDataRepresentation +): List = + this.map { + it.toFinalRepresentation(ngsiLdDataRepresentation) + } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentation.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentation.kt new file mode 100644 index 0000000000..b679681d8e --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentation.kt @@ -0,0 +1,34 @@ +package com.egm.stellio.shared.model + +import com.egm.stellio.shared.util.GEO_JSON_MEDIA_TYPE +import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE +import org.springframework.http.MediaType + +/** + * Wrapper data class used to convey possible NGSI-LD Data Representations for entities as defined in 4.5 + */ +data class NgsiLdDataRepresentation( + val entityRepresentation: EntityRepresentation, + val attributeRepresentation: AttributeRepresentation, + val includeSysAttrs: Boolean +) + +enum class AttributeRepresentation { + NORMALIZED, + SIMPLIFIED +} + +enum class EntityRepresentation { + JSON_LD, + JSON, + GEO_JSON; + + companion object { + fun forMediaType(mediaType: MediaType): EntityRepresentation = + when (mediaType) { + JSON_LD_MEDIA_TYPE -> JSON_LD + GEO_JSON_MEDIA_TYPE -> GEO_JSON + else -> JSON + } + } +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt index 264f71b00f..f895afa952 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt @@ -15,7 +15,6 @@ import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.util.MimeTypeUtils import org.springframework.util.MultiValueMap -import org.springframework.web.server.NotAcceptableStatusException import reactor.core.publisher.Mono import java.time.ZonedDateTime import java.time.format.DateTimeParseException @@ -24,6 +23,7 @@ import java.util.regex.Pattern const val RESULTS_COUNT_HEADER = "NGSILD-Results-Count" const val JSON_LD_CONTENT_TYPE = "application/ld+json" +const val GEO_JSON_CONTENT_TYPE = "application/geo+json" const val JSON_MERGE_PATCH_CONTENT_TYPE = "application/merge-patch+json" const val QUERY_PARAM_COUNT: String = "count" const val QUERY_PARAM_OFFSET: String = "offset" @@ -40,6 +40,7 @@ const val QUERY_PARAM_OPTIONS_KEYVALUES_VALUE: String = "keyValues" const val QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE: String = "noOverwrite" const val QUERY_PARAM_OPTIONS_OBSERVEDAT_VALUE: String = "observedAt" val JSON_LD_MEDIA_TYPE = MediaType.valueOf(JSON_LD_CONTENT_TYPE) +val GEO_JSON_MEDIA_TYPE = MediaType.valueOf(GEO_JSON_CONTENT_TYPE) val qPattern: Pattern = Pattern.compile("([^();|]+)") val typeSelectionRegex: Regex = """([^(),;|]+)""".toRegex() @@ -169,6 +170,21 @@ fun parseAndExpandRequestParameter(requestParam: String?, contextLink: String): JsonLdUtils.expandJsonLdTerm(it.trim(), contextLink) }.toSet() +fun parseRepresentations( + optionsParam: List, + acceptMediaType: MediaType +): NgsiLdDataRepresentation { + val includeSysAttrs = optionsParam.contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) + val attributeRepresentation = optionsParam.contains(QUERY_PARAM_OPTIONS_KEYVALUES_VALUE) + .let { if (it) AttributeRepresentation.SIMPLIFIED else AttributeRepresentation.NORMALIZED } + + return NgsiLdDataRepresentation( + EntityRepresentation.forMediaType(acceptMediaType), + attributeRepresentation, + includeSysAttrs + ) +} + fun validateIdPattern(idPattern: String?): Either = idPattern?.let { runCatching { @@ -200,26 +216,24 @@ fun parsePaginationParameters( return PaginationQuery(offset, limit, count).right() } -fun getApplicableMediaType(httpHeaders: HttpHeaders): MediaType = +fun getApplicableMediaType(httpHeaders: HttpHeaders): Either = httpHeaders.accept.getApplicable() /** - * Return the applicable media type among JSON_LD_MEDIA_TYPE and MediaType.APPLICATION_JSON. - * - * @throws NotAcceptableStatusException if none of the above media types are applicable + * Return the applicable media type among JSON_LD_MEDIA_TYPE, GEO_JSON_MEDIA_TYPE and MediaType.APPLICATION_JSON. */ -fun List.getApplicable(): MediaType { +fun List.getApplicable(): Either { if (this.isEmpty()) - return MediaType.APPLICATION_JSON + return MediaType.APPLICATION_JSON.right() MimeTypeUtils.sortBySpecificity(this) val mediaType = this.find { - it.includes(MediaType.APPLICATION_JSON) || it.includes(JSON_LD_MEDIA_TYPE) - } ?: throw NotAcceptableStatusException(listOf(MediaType.APPLICATION_JSON, JSON_LD_MEDIA_TYPE)) + it.includes(MediaType.APPLICATION_JSON) || it.includes(JSON_LD_MEDIA_TYPE) || it.includes(GEO_JSON_MEDIA_TYPE) + } ?: return NotAcceptableException("Unsupported Accept header value: ${this.joinToString(",")}").left() // as per 6.3.4, application/json has a higher precedence than application/ld+json return if (mediaType.includes(MediaType.APPLICATION_JSON)) - MediaType.APPLICATION_JSON + MediaType.APPLICATION_JSON.right() else - JSON_LD_MEDIA_TYPE + JSON_LD_MEDIA_TYPE.right() } fun String.parseTimeParameter(errorMsg: String): Either = diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt index 033a4a1274..abeb14569f 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt @@ -10,7 +10,6 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE_TERM import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEO_PROPERTIES_TERMS import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SYSATTRS_TERMS import com.egm.stellio.shared.util.JsonLdUtils.buildNonReifiedDateTime import com.egm.stellio.shared.util.JsonLdUtils.getPropertyValueFromMapAsDateTime import com.egm.stellio.shared.util.JsonUtils.deserializeAs @@ -374,7 +373,7 @@ object JsonLdUtils { } else it.value } - fun compact( + fun compactEntity( jsonLdEntity: JsonLdEntity, context: String? = null, mediaType: MediaType = JSON_LD_MEDIA_TYPE @@ -392,7 +391,7 @@ object JsonLdUtils { .mapValues(restoreGeoPropertyValue()) } - fun compact( + fun compactEntity( jsonLdEntity: JsonLdEntity, contexts: List, mediaType: MediaType = JSON_LD_MEDIA_TYPE @@ -412,20 +411,11 @@ object JsonLdUtils { fun compactEntities( entities: List, - useSimplifiedRepresentation: Boolean, - includeSysAttrs: Boolean, context: String, mediaType: MediaType ): List = entities.map { - if (useSimplifiedRepresentation) - compact(it, context, mediaType).toKeyValues() - else - compact(it, context, mediaType) - }.map { - if (!includeSysAttrs) - it.withoutSysAttrs() - else it + compactEntity(it, context, mediaType) } fun compactTerms(terms: List, contexts: List): List = @@ -669,62 +659,6 @@ fun ExpandedAttributeInstances.getSingleEntry(): ExpandedAttributeInstance { return this[0] } -fun CompactedJsonLdEntity.toKeyValues(): Map = - this.mapValues { (_, value) -> simplifyRepresentation(value) } - -private fun simplifyRepresentation(value: Any): Any { - return when (value) { - // entity property value is always a Map - is Map<*, *> -> simplifyValue(value as Map) - is List<*> -> value.map { - when (it) { - is Map<*, *> -> simplifyValue(it as Map) - // we keep @context value as it is (List) - else -> it - } - } - // we keep id and type values as they are (String) - else -> value - } -} - -private fun simplifyValue(value: Map): Any { - return when (value["type"]) { - "Property", "GeoProperty" -> value.getOrDefault("value", value) - "Relationship" -> value.getOrDefault("object", value) - else -> value - } -} - -fun CompactedJsonLdEntity.withoutSysAttrs(): Map = - this.filter { - !NGSILD_SYSATTRS_TERMS.contains(it.key) - }.mapValues { - when (it.value) { - is Map<*, *> -> (it.value as Map<*, *>).minus(NGSILD_SYSATTRS_TERMS) - is List<*> -> (it.value as List<*>).map { valueInstance -> - when (valueInstance) { - is Map<*, *> -> valueInstance.minus(NGSILD_SYSATTRS_TERMS) - // we keep @context value as it is (List) - else -> valueInstance - } - } - else -> it.value - } - } - -fun CompactedJsonLdEntity.toFinalRepresentation( - includeSysAttrs: Boolean, - useSimplifiedRepresentation: Boolean -): CompactedJsonLdEntity = - this.let { - if (!includeSysAttrs) it.withoutSysAttrs() - else it - }.let { - if (useSimplifiedRepresentation) it.toKeyValues() - else it - } - fun geoPropertyToWKT(jsonFragment: Map): Map { for (geoProperty in NGSILD_GEO_PROPERTIES_TERMS) { if (jsonFragment.containsKey(geoProperty)) { @@ -747,11 +681,6 @@ fun geoPropertyToWKT(jsonFragment: Map): Map { return jsonFragment } -fun Map.addDateTimeProperty(propertyKey: String, dateTime: ZonedDateTime?): Map = - if (dateTime != null) - this.plus(propertyKey to buildNonReifiedDateTime(dateTime)) - else this - fun Map.addSysAttrs( withSysAttrs: Boolean, createdAt: ZonedDateTime, diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt index 87877d79ed..7b6eb69a3e 100644 --- a/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt +++ b/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt @@ -5,6 +5,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_PROPERTY import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NAME_PROPERTY import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity +import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat @@ -15,6 +16,108 @@ import org.junit.jupiter.api.fail @OptIn(ExperimentalCoroutinesApi::class) class JsonLdEntityTests { + private val normalizedJson = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "brandName": { + "type": "Property", + "value": "Mercedes" + }, + "isParked": { + "type": "Relationship", + "object": "urn:ngsi-ld:OffStreetParking:Downtown1", + "observedAt": "2017-07-29T12:00:04Z", + "providedBy": { + "type": "Relationship", + "object": "urn:ngsi-ld:Person:Bob" + } + }, + "location": { + "type": "GeoProperty", + "value": { + "type": "Point", + "coordinates": [ + 24.30623, + 60.07966 + ] + } + }, + "@context": [ + "https://example.org/ngsi-ld/latest/commonTerms.jsonld", + "https://example.org/ngsi-ld/latest/vehicle.jsonld", + "https://example.org/ngsi-ld/latest/parking.jsonld", + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld" + ] + } + """.trimIndent() + + private val simplifiedJson = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "brandName": "Mercedes", + "isParked": "urn:ngsi-ld:OffStreetParking:Downtown1", + "location": { + "type": "Point", + "coordinates": [ + 24.30623, + 60.07966 + ] + }, + "@context": [ + "https://example.org/ngsi-ld/latest/commonTerms.jsonld", + "https://example.org/ngsi-ld/latest/vehicle.jsonld", + "https://example.org/ngsi-ld/latest/parking.jsonld", + "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld" + ] + } + """.trimIndent() + + private val normalizedMultiAttributeJson = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "speed": [ + { + "type": "Property", + "datasetId": "urn:ngsi-ld:Dataset:01", + "value": 10 + }, + { + "type": "Property", + "datasetId": "urn:ngsi-ld:Dataset:02", + "value": 11 + } + ], + "hasOwner": [ + { + "type": "Relationship", + "datasetId": "urn:ngsi-ld:Dataset:01", + "object": "urn:ngsi-ld:Person:John" + }, + { + "type": "Relationship", + "datasetId": "urn:ngsi-ld:Dataset:02", + "object": "urn:ngsi-ld:Person:Jane" + } + ] + } + """.trimIndent() + + private val simplifiedMultiAttributeJson = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "speed": [ 10, 11 ], + "hasOwner": [ "urn:ngsi-ld:Person:John", "urn:ngsi-ld:Person:Jane" ] + } + """.trimIndent() + @Test fun `it should find an expanded attribute contained in the entity`() { val jsonLdEntity = JsonLdEntity( @@ -89,4 +192,96 @@ class JsonLdEntityTests { assertThat(nameAttributeInstances).hasSize(1) assertThat(nameAttributeInstances[0]).containsKey(NGSILD_CREATED_AT_PROPERTY) } + + @Test + fun `it should simplify a compacted entity`() { + val normalizedMap = normalizedJson.deserializeAsMap() + val simplifiedMap = simplifiedJson.deserializeAsMap() + + val resultMap = normalizedMap.toKeyValues() + + assertEquals(simplifiedMap, resultMap) + } + + @Test + fun `it should simplify a compacted entity with multi-attributes`() { + val normalizedMap = normalizedMultiAttributeJson.deserializeAsMap() + val simplifiedMap = simplifiedMultiAttributeJson.deserializeAsMap() + + val resultMap = normalizedMap.toKeyValues() + + assertEquals(simplifiedMap, resultMap) + } + + @Test + fun `it should return a simplified entity with sysAttrs`() { + val inputEntity = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "createdAt": "2023-11-25T08:00:00Z", + "modifiedAt": "2023-11-25T09:00:00Z", + "brandName": { + "type": "Property", + "value": "Mercedes" + } + } + """.trimIndent().deserializeAsMap() + val expectedEntity = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "createdAt": "2023-11-25T08:00:00Z", + "modifiedAt": "2023-11-25T09:00:00Z", + "brandName": "Mercedes" + } + """.trimIndent().deserializeAsMap() + + val simplifiedEntity = inputEntity.toFinalRepresentation( + NgsiLdDataRepresentation( + EntityRepresentation.JSON, + AttributeRepresentation.SIMPLIFIED, + includeSysAttrs = true + ) + ) + + assertEquals(expectedEntity, simplifiedEntity) + } + + @Test + fun `it should return a simplified entity without sysAttrs`() { + val inputEntity = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "createdAt": "2023-11-25T08:00:00Z", + "modifiedAt": "2023-11-25T09:00:00Z", + "brandName": { + "type": "Property", + "value": "Mercedes" + } + } + """.trimIndent().deserializeAsMap() + val expectedEntity = + """ + { + "id": "urn:ngsi-ld:Vehicle:A4567", + "type": "Vehicle", + "brandName": "Mercedes" + } + """.trimIndent().deserializeAsMap() + + val simplifiedEntity = inputEntity.toFinalRepresentation( + NgsiLdDataRepresentation( + EntityRepresentation.JSON, + AttributeRepresentation.SIMPLIFIED, + includeSysAttrs = false + ) + ) + + assertEquals(expectedEntity, simplifiedEntity) + } } diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonLdUtilsTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonLdUtilsTests.kt index 838b77898e..a7742f7561 100644 --- a/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonLdUtilsTests.kt +++ b/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonLdUtilsTests.kt @@ -7,7 +7,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_HAS_OBJECT import com.egm.stellio.shared.util.JsonLdUtils.addCoreContextIfMissing -import com.egm.stellio.shared.util.JsonLdUtils.compact +import com.egm.stellio.shared.util.JsonLdUtils.compactEntity import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdFragment import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm import com.egm.stellio.shared.util.JsonLdUtils.extractContextFromInput @@ -61,91 +61,6 @@ class JsonLdUtilsTests { } """.trimIndent() - private val simplifiedJson = - """ - { - "id": "urn:ngsi-ld:Vehicle:A4567", - "type": "Vehicle", - "brandName": "Mercedes", - "isParked": "urn:ngsi-ld:OffStreetParking:Downtown1", - "location": { - "type": "Point", - "coordinates": [ - 24.30623, - 60.07966 - ] - }, - "@context": [ - "https://example.org/ngsi-ld/latest/commonTerms.jsonld", - "https://example.org/ngsi-ld/latest/vehicle.jsonld", - "https://example.org/ngsi-ld/latest/parking.jsonld", - "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld" - ] - } - """.trimIndent() - - private val normalizedMultiAttributeJson = - """ - { - "id": "urn:ngsi-ld:Vehicle:A4567", - "type": "Vehicle", - "speed": [ - { - "type": "Property", - "datasetId": "urn:ngsi-ld:Dataset:01", - "value": 10 - }, - { - "type": "Property", - "datasetId": "urn:ngsi-ld:Dataset:02", - "value": 11 - } - ], - "hasOwner": [ - { - "type": "Relationship", - "datasetId": "urn:ngsi-ld:Dataset:01", - "object": "urn:ngsi-ld:Person:John" - }, - { - "type": "Relationship", - "datasetId": "urn:ngsi-ld:Dataset:02", - "object": "urn:ngsi-ld:Person:Jane" - } - ] - } - """.trimIndent() - - private val simplifiedMultiAttributeJson = - """ - { - "id": "urn:ngsi-ld:Vehicle:A4567", - "type": "Vehicle", - "speed": [ 10, 11 ], - "hasOwner": [ "urn:ngsi-ld:Person:John", "urn:ngsi-ld:Person:Jane" ] - } - """.trimIndent() - - @Test - fun `it should simplify a compacted entity`() { - val normalizedMap = mapper.readValue(normalizedJson, Map::class.java) - val simplifiedMap = mapper.readValue(simplifiedJson, Map::class.java) - - val resultMap = (normalizedMap as CompactedJsonLdEntity).toKeyValues() - - assertEquals(simplifiedMap, resultMap) - } - - @Test - fun `it should simplify a compacted entity with multi-attributes`() { - val normalizedMap = mapper.readValue(normalizedMultiAttributeJson, Map::class.java) - val simplifiedMap = mapper.readValue(simplifiedMultiAttributeJson, Map::class.java) - - val resultMap = (normalizedMap as CompactedJsonLdEntity).toKeyValues() - - assertEquals(simplifiedMap, resultMap) - } - @Test fun `it should filter a JSON-LD Map on the attributes specified as well as the mandatory attributes`() { val normalizedMap = mapper.readValue(normalizedJson, Map::class.java) @@ -318,7 +233,7 @@ class JsonLdUtilsTests { """.trimIndent() val jsonLdEntity = JsonLdUtils.expandJsonLdEntity(entity, DEFAULT_CONTEXTS) - val compactedEntity = compact(jsonLdEntity, DEFAULT_CONTEXTS, MediaType.APPLICATION_JSON) + val compactedEntity = compactEntity(jsonLdEntity, DEFAULT_CONTEXTS, MediaType.APPLICATION_JSON) assertTrue(mapper.writeValueAsString(compactedEntity).matchContent(entity)) } @@ -345,7 +260,7 @@ class JsonLdUtilsTests { """.trimIndent() val jsonLdEntity = JsonLdUtils.expandJsonLdEntity(entity, DEFAULT_CONTEXTS) - val compactedEntity = compact(jsonLdEntity, DEFAULT_CONTEXTS) + val compactedEntity = compactEntity(jsonLdEntity, DEFAULT_CONTEXTS) assertTrue(mapper.writeValueAsString(compactedEntity).matchContent(expectedEntity)) } diff --git a/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/AssertionUtils.kt b/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/AssertionUtils.kt index 9bfecd7407..dcc67743d3 100644 --- a/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/AssertionUtils.kt +++ b/shared/src/testFixtures/kotlin/com/egm/stellio/shared/util/AssertionUtils.kt @@ -2,10 +2,34 @@ package com.egm.stellio.shared.util import arrow.core.Either import com.egm.stellio.shared.model.APIException +import com.fasterxml.jackson.core.filter.FilteringParserDelegate +import com.fasterxml.jackson.core.filter.TokenFilter import org.junit.jupiter.api.Assertions.* +import java.io.ByteArrayInputStream -fun assertJsonPayloadsAreEqual(expectation: String, actual: String) = - assertEquals(mapper.readTree(expectation), mapper.readTree(actual)) +fun assertJsonPayloadsAreEqual(expectation: String, actual: String, ignoredKeys: Set = emptySet()) { + val tokenFilter: TokenFilter = object : TokenFilter() { + override fun includeProperty(name: String): TokenFilter? = + if (ignoredKeys.contains(name)) null + else INCLUDE_ALL + } + + val filteredExpectation = FilteringParserDelegate( + mapper.createParser(ByteArrayInputStream(expectation.toByteArray())), + tokenFilter, + TokenFilter.Inclusion.INCLUDE_ALL_AND_PATH, + true + ) + + val filteredActual = FilteringParserDelegate( + mapper.createParser(ByteArrayInputStream(actual.toByteArray())), + tokenFilter, + TokenFilter.Inclusion.INCLUDE_ALL_AND_PATH, + true + ) + + assertEquals(mapper.readTree(filteredExpectation), mapper.readTree(filteredActual)) +} fun Either.shouldSucceedWith(assertions: (T) -> Unit) = fold({ diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt index 2710fa9c72..6e6c35a4e5 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/NotificationService.kt @@ -5,12 +5,12 @@ import arrow.core.raise.either import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.CompactedJsonLdEntity import com.egm.stellio.shared.model.JsonLdEntity +import com.egm.stellio.shared.model.toKeyValues import com.egm.stellio.shared.util.ExpandedTerm -import com.egm.stellio.shared.util.JsonLdUtils.compact +import com.egm.stellio.shared.util.JsonLdUtils.compactEntity import com.egm.stellio.shared.util.JsonLdUtils.filterJsonLdEntityOnAttributes import com.egm.stellio.shared.util.JsonUtils.serializeObject import com.egm.stellio.shared.util.getTenantFromContext -import com.egm.stellio.shared.util.toKeyValues import com.egm.stellio.shared.web.DEFAULT_TENANT_URI import com.egm.stellio.shared.web.NGSILD_TENANT_HEADER import com.egm.stellio.subscription.model.Notification @@ -41,7 +41,7 @@ class NotificationService( .map { val filteredEntity = filterJsonLdEntityOnAttributes(jsonLdEntity, it.notification.attributes?.toSet().orEmpty()) - val compactedEntity = compact( + val compactedEntity = compactEntity( JsonLdEntity(filteredEntity, it.contexts), it.contexts, MediaType.valueOf(it.notification.endpoint.accept.accept) diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt index a87964c91f..b095949d91 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt @@ -68,7 +68,7 @@ class SubscriptionHandler( @RequestParam params: MultiValueMap ): ResponseEntity<*> = either { val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() val sub = getSubFromSecurityContext() val includeSysAttrs = params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) @@ -107,7 +107,7 @@ class SubscriptionHandler( ): ResponseEntity<*> = either { val includeSysAttrs = options.filter { it.contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) }.isPresent val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders).bind() - val mediaType = getApplicableMediaType(httpHeaders) + val mediaType = getApplicableMediaType(httpHeaders).bind() checkSubscriptionExists(subscriptionId).bind()