diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml index f467d3c62..9137ecca1 100644 --- a/search-service/config/detekt/baseline.xml +++ b/search-service/config/detekt/baseline.xml @@ -10,14 +10,13 @@ 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:EntityEventService.kt$EntityEventService$private fun publishAttributeChangeEvent( sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair<List<ExpandedTerm>, String>, attributeOperationResult: SucceededAttributeOperationResult ) - 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: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.FORMAT, QP.TYPE, QP.ATTRS, QP.GEOMETRY_PROPERTY, QP.LANG, QP.CONTAINED_BY, QP.JOIN, QP.JOIN_LEVEL, QP.DATASET_ID, ], notImplemented = [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.FORMAT, 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.PICK, QP.OMIT, QP.EXPAND_VALUES, QP.CSF, QP.ENTITY_MAP, QP.LOCAL, QP.VIA] ) @RequestParam queryParams: MultiValueMap<String, String> ): ResponseEntity<*> 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> LongMethod:TemporalQueryServiceTests.kt$TemporalQueryServiceTests$@Test fun `it should query temporal entities as requested by query params`() LongMethod:TemporalQueryServiceTests.kt$TemporalQueryServiceTests$@Test fun `it should return an empty list for an attribute if it has no temporal values`() - LongMethod:TemporalScopeBuilderTests.kt$TemporalScopeBuilderTests$@Test fun `it should build an aggregated temporal representation of scopes`() LongMethod:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context) LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeAndProperty: Pair<ZonedDateTime, TemporalProperty>, value: Triple<String?, Double?, WKTCoordinates?>, payload: ExpandedAttributeInstance, sub: String? ) LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, modifiedAt: ZonedDateTime? = null, attributeMetadata: AttributeMetadata, payload: ExpandedAttributeInstance, time: ZonedDateTime, sub: String? = null ) 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 cbbdf667c..048862f44 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 @@ -105,7 +105,7 @@ class EntityAccessControlHandler( val compactedEntities = compactEntities(entities, contexts) - val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType).bind() buildQueryResponse( compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), count, @@ -150,7 +150,7 @@ class EntityAccessControlHandler( val compactedEntities = compactEntities(entities, contexts) - val ngsiLdDataRepresentation = parseRepresentations(params, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(params, mediaType).bind() buildQueryResponse( compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), count, @@ -196,7 +196,7 @@ class EntityAccessControlHandler( val compactedEntities = compactEntities(entities, contexts) - val ngsiLdDataRepresentation = parseRepresentations(params, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(params, mediaType).bind() buildQueryResponse( compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), count, 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 223cec0e4..f020f47b0 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 @@ -95,7 +95,7 @@ class EntityAttributeService( private val logger = LoggerFactory.getLogger(javaClass) @Transactional - suspend fun create(attribute: Attribute): Either = + suspend fun create(attribute: Attribute): Either = databaseClient.sql( """ INSERT INTO temporal_entity_attribute @@ -110,6 +110,7 @@ class EntityAttributeService( attribute_value_type = :attribute_value_type, modified_at = :created_at, payload = :payload + RETURNING id """.trimIndent() ) .bind("id", attribute.id) @@ -120,7 +121,7 @@ class EntityAttributeService( .bind("created_at", attribute.createdAt) .bind("dataset_id", attribute.datasetId) .bind("payload", attribute.payload) - .execute() + .oneToResult { row -> toUuid(row["id"]) } @Transactional suspend fun updateOnUpdate( @@ -215,10 +216,10 @@ class EntityAttributeService( createdAt = createdAt, payload = Json.of(serializeObject(attributePayload)) ) - create(attribute).bind() + val attributeUuid = create(attribute).bind() val attributeInstance = AttributeInstance( - attributeUuid = attribute.id, + attributeUuid = attributeUuid, timeProperty = AttributeInstance.TemporalProperty.CREATED_AT, time = createdAt, attributeMetadata = attributeMetadata, @@ -229,7 +230,7 @@ class EntityAttributeService( if (attributeMetadata.observedAt != null) { val attributeObservedAtInstance = AttributeInstance( - attributeUuid = attribute.id, + attributeUuid = attributeUuid, time = attributeMetadata.observedAt, attributeMetadata = attributeMetadata, payload = attributePayload diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt index 5dfb34e01..f35763175 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityHandler.kt @@ -194,11 +194,11 @@ class EntityHandler( @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.OPTIONS, QP.FORMAT, 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] + notImplemented = [QP.PICK, QP.OMIT, QP.EXPAND_VALUES, QP.CSF, QP.ENTITY_MAP, QP.LOCAL, QP.VIA] ) @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { @@ -264,7 +264,7 @@ class EntityHandler( mergedEntities ?: emptyList() } - val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType).bind() buildQueryResponse( mergedEntities.toFinalRepresentation(ngsiLdDataRepresentation), maxCount, @@ -288,10 +288,10 @@ class EntityHandler( @PathVariable entityId: URI, @AllowedParameters( implemented = [ - QP.OPTIONS, QP.TYPE, QP.ATTRS, QP.GEOMETRY_PROPERTY, + QP.OPTIONS, QP.FORMAT, 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] + notImplemented = [QP.PICK, QP.OMIT, QP.ENTITY_MAP, QP.LOCAL, QP.VIA] ) @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { @@ -358,7 +358,7 @@ class EntityHandler( val mergedEntityWithLinkedEntities = linkedEntityService.processLinkedEntities(mergedEntity, entitiesQuery, sub.getOrNull()).bind() - val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType).bind() prepareGetSuccessResponseHeaders(mediaType, contexts) .let { val body = if (mergedEntityWithLinkedEntities.size == 1) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt index 3d5c9913d..5c1d49247 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/web/EntityOperationHandler.kt @@ -281,7 +281,7 @@ class EntityOperationHandler( val compactedEntities = compactEntities(filteredEntities, contexts) - val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType).bind() .copy(languageFilter = query.lang) buildQueryResponse( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt index 9eacfef87..0310a7949 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/scope/ScopeService.kt @@ -24,6 +24,7 @@ import com.egm.stellio.search.entity.model.SucceededAttributeOperationResult import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.search.temporal.util.WHOLE_TIME_RANGE_DURATION import com.egm.stellio.search.temporal.util.composeAggregationSelectClause import com.egm.stellio.shared.model.APIException @@ -136,7 +137,7 @@ class ScopeService( if (temporalEntitiesQuery.isAggregatedWithDefinedDuration()) sqlQueryBuilder.append(" GROUP BY entity_id, start") - else if (temporalEntitiesQuery.withAggregatedValues) + else if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES) sqlQueryBuilder.append(" GROUP BY entity_id") if (temporalQuery.hasLastN()) // in order to get first or last instances, need to order by time @@ -161,7 +162,7 @@ class ScopeService( temporalEntitiesQuery: TemporalEntitiesQuery, origin: ZonedDateTime? ): String = when { - temporalEntitiesQuery.withAggregatedValues -> { + temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES -> { val temporalQuery = temporalEntitiesQuery.temporalQuery val aggrPeriodDuration = temporalQuery.aggrPeriodDuration val allAggregates = temporalQuery.aggrMethods?.composeAggregationSelectClause(AttributeValueType.ARRAY) @@ -215,7 +216,7 @@ class ScopeService( row: Map, temporalEntitiesQuery: TemporalEntitiesQuery ): ScopeInstanceResult = - if (temporalEntitiesQuery.withAggregatedValues) { + if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES) { val startDateTime = toZonedDateTime(row["start"]) val endDateTime = if (!temporalEntitiesQuery.isAggregatedWithDefinedDuration()) @@ -231,7 +232,7 @@ class ScopeService( entityId = toUri(row["entity_id"]), values = values ) - } else if (temporalEntitiesQuery.withTemporalValues) { + } else if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES) { SimplifiedScopeInstanceResult( entityId = toUri(row["entity_id"]), scopes = toList(row["value"]), diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt b/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt index 0ab5b7113..f6aefa286 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilder.kt @@ -3,6 +3,7 @@ package com.egm.stellio.search.scope import com.egm.stellio.search.entity.model.Entity import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LIST import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE @@ -27,12 +28,12 @@ object TemporalScopeBuilder { // if no history but entity has a scope, add an empty scope list (no history in the given time range) else if (scopeInstances.isEmpty()) mapOf(NGSILD_SCOPE_PROPERTY to emptyList()) - else if (temporalEntitiesQuery.withAggregatedValues) + else if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES) buildScopeAggregatedRepresentation( scopeInstances, temporalEntitiesQuery.temporalQuery.aggrMethods!! ) - else if (temporalEntitiesQuery.withTemporalValues) + else if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES) buildScopeSimplifiedRepresentation(scopeInstances) else buildScopeFullRepresentation(scopeInstances) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalEntitiesQuery.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalEntitiesQuery.kt index 5843cf52a..cb85b85c1 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalEntitiesQuery.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/model/TemporalEntitiesQuery.kt @@ -3,6 +3,7 @@ package com.egm.stellio.search.temporal.model import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.search.entity.model.EntitiesQueryFromGet import com.egm.stellio.search.entity.model.EntitiesQueryFromPost +import com.egm.stellio.search.temporal.util.TemporalRepresentation import java.time.Duration import java.time.Period import java.time.temporal.TemporalAmount @@ -10,12 +11,11 @@ import java.time.temporal.TemporalAmount sealed class TemporalEntitiesQuery( open val entitiesQuery: EntitiesQuery, open val temporalQuery: TemporalQuery, - open val withTemporalValues: Boolean, - open val withAudit: Boolean, - open val withAggregatedValues: Boolean + open val temporalRepresentation: TemporalRepresentation, + open val withAudit: Boolean ) { fun isAggregatedWithDefinedDuration(): Boolean = - withAggregatedValues && + temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES && temporalQuery.aggrPeriodDuration != null && temporalQuery.aggrPeriodDuration != "PT0S" @@ -36,15 +36,13 @@ sealed class TemporalEntitiesQuery( data class TemporalEntitiesQueryFromGet( override val entitiesQuery: EntitiesQueryFromGet, override val temporalQuery: TemporalQuery, - override val withTemporalValues: Boolean, - override val withAudit: Boolean, - override val withAggregatedValues: Boolean -) : TemporalEntitiesQuery(entitiesQuery, temporalQuery, withTemporalValues, withAudit, withAggregatedValues) + override val temporalRepresentation: TemporalRepresentation, + override val withAudit: Boolean +) : TemporalEntitiesQuery(entitiesQuery, temporalQuery, temporalRepresentation, withAudit) data class TemporalEntitiesQueryFromPost( override val entitiesQuery: EntitiesQueryFromPost, override val temporalQuery: TemporalQuery, - override val withTemporalValues: Boolean, - override val withAudit: Boolean, - override val withAggregatedValues: Boolean -) : TemporalEntitiesQuery(entitiesQuery, temporalQuery, withTemporalValues, withAudit, withAggregatedValues) + override val temporalRepresentation: TemporalRepresentation, + override val withAudit: Boolean +) : TemporalEntitiesQuery(entitiesQuery, temporalQuery, temporalRepresentation, withAudit) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceService.kt index d718bd9ee..6e189f61e 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceService.kt @@ -28,6 +28,7 @@ import com.egm.stellio.search.temporal.model.SimplifiedAttributeInstanceResult import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.search.temporal.model.TemporalQuery.Timerel +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.search.temporal.util.WHOLE_TIME_RANGE_DURATION import com.egm.stellio.search.temporal.util.composeAggregationSelectClause import com.egm.stellio.shared.model.APIException @@ -174,7 +175,7 @@ class AttributeInstanceService( sqlQueryBuilder.append(composeSearchSelectStatement(temporalQuery, attributes, origin)) - if (!temporalEntitiesQuery.withTemporalValues && !temporalEntitiesQuery.withAggregatedValues) + if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.NORMALIZED) sqlQueryBuilder.append(", payload") if (temporalQuery.timeproperty == OBSERVED_AT) @@ -204,7 +205,7 @@ class AttributeInstanceService( if (temporalEntitiesQuery.isAggregatedWithDefinedDuration()) sqlQueryBuilder.append(" GROUP BY temporal_entity_attribute, start") - else if (temporalEntitiesQuery.withAggregatedValues) + else if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES) sqlQueryBuilder.append(" GROUP BY temporal_entity_attribute") if (temporalQuery.hasLastN()) @@ -319,7 +320,7 @@ class AttributeInstanceService( row: Map, temporalEntitiesQuery: TemporalEntitiesQuery ): AttributeInstanceResult = - if (temporalEntitiesQuery.withAggregatedValues) { + if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES) { val startDateTime = toZonedDateTime(row["start"]) val endDateTime = if (!temporalEntitiesQuery.isAggregatedWithDefinedDuration()) @@ -335,7 +336,7 @@ class AttributeInstanceService( attributeUuid = toUuid(row["temporal_entity_attribute"]), values = values ) - } else if (temporalEntitiesQuery.withTemporalValues) + } else if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES) SimplifiedAttributeInstanceResult( attributeUuid = toUuid(row["temporal_entity_attribute"]), // the type of the value of a property may have changed in the history (e.g., from number to string) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationService.kt index 1b9d166f1..96d002601 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationService.kt @@ -5,6 +5,7 @@ import com.egm.stellio.search.temporal.model.AttributeInstanceResult import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.search.temporal.util.AttributesWithInstances +import com.egm.stellio.search.temporal.util.TemporalRepresentation import java.time.ZonedDateTime typealias Range = Pair @@ -47,12 +48,13 @@ object TemporalPaginationService { val temporalQuery = query.temporalQuery val attributesTimeRanges = attributeInstancesWhoReachedLimit.map { - it.first().getComparableTime() to if (query.withAggregatedValues) { - val lastInstance = it.last() as AggregatedAttributeInstanceResult - lastInstance.values.first().endDateTime - } else { - it.last().getComparableTime() - } + it.first().getComparableTime() to + if (query.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES) { + val lastInstance = it.last() as AggregatedAttributeInstanceResult + lastInstance.values.first().endDateTime + } else { + it.last().getComparableTime() + } } if (temporalQuery.hasLastN()) { diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt index 8f0e742ef..57535e597 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryService.kt @@ -17,6 +17,7 @@ import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery import com.egm.stellio.search.temporal.service.TemporalPaginationService.getPaginatedAttributeWithInstancesAndRange import com.egm.stellio.search.temporal.util.AttributesWithInstances import com.egm.stellio.search.temporal.util.TemporalEntityBuilder +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.search.temporal.web.Range import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.ExpandedEntity @@ -93,7 +94,7 @@ class TemporalQueryService( // - timeAt if it is provided // - the oldest value if not (timeAt is optional if querying a temporal entity by id) - return if (!temporalEntitiesQuery.withAggregatedValues) + return if (temporalEntitiesQuery.temporalRepresentation != TemporalRepresentation.AGGREGATED_VALUES) null else if (temporalQuery.timeAt != null) temporalQuery.timeAt @@ -198,7 +199,7 @@ class TemporalQueryService( // when retrieved from DB, values of geo-properties are encoded in WKT and won't be automatically // transformed during compaction as it is not done for temporal values, so it is done now if (it.key == Attribute.AttributeValueType.GEOMETRY && - temporalEntitiesQuery.withTemporalValues + temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES ) { it.value.map { attributeInstanceResult -> attributeInstanceResult as SimplifiedAttributeInstanceResult diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilder.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilder.kt index 4800e42fb..d5e96b895 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilder.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilder.kt @@ -68,10 +68,10 @@ object TemporalEntityBuilder { attributeAndResultsMap: AttributesWithInstances, temporalEntitiesQuery: TemporalEntitiesQuery, ): Map = - if (temporalEntitiesQuery.withTemporalValues) { + if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES) { val attributes = buildAttributesSimplifiedRepresentation(attributeAndResultsMap) mergeSimplifiedTemporalAttributesOnAttributeName(attributes) - } else if (temporalEntitiesQuery.withAggregatedValues) { + } else if (temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES) { val attributes = buildAttributesAggregatedRepresentation( attributeAndResultsMap, temporalEntitiesQuery.temporalQuery.aggrMethods!! diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtils.kt index 494a8a5ee..5855d2193 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtils.kt @@ -20,8 +20,9 @@ import com.egm.stellio.search.temporal.model.TemporalQuery.Timerel 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.InvalidRequestException +import com.egm.stellio.shared.queryparameter.OptionsValue import com.egm.stellio.shared.queryparameter.QueryParameter -import com.egm.stellio.shared.util.OptionsParamValue import com.egm.stellio.shared.util.hasValueInOptionsParam import com.egm.stellio.shared.util.parseTimeParameter import org.springframework.util.MultiValueMap @@ -42,31 +43,21 @@ fun composeTemporalEntitiesQueryFromGet( requestParams, contexts ).bind() - if (inQueryEntities) entitiesQueryFromGet.validateMinimalQueryEntitiesParameters().bind() - - val withTemporalValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), - OptionsParamValue.TEMPORAL_VALUES - ) + val temporalRepresentation = extractTemporalRepresentation(requestParams).bind() val withAudit = hasValueInOptionsParam( Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), - OptionsParamValue.AUDIT - ) - val withAggregatedValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), - OptionsParamValue.AGGREGATED_VALUES - ) + OptionsValue.AUDIT + ).bind() val temporalQuery = - buildTemporalQuery(requestParams, defaultPagination, inQueryEntities, withAggregatedValues).bind() + buildTemporalQuery(requestParams, defaultPagination, inQueryEntities, temporalRepresentation).bind() TemporalEntitiesQueryFromGet( entitiesQuery = entitiesQueryFromGet, temporalQuery = temporalQuery, - withTemporalValues = withTemporalValues, - withAudit = withAudit, - withAggregatedValues = withAggregatedValues + temporalRepresentation = temporalRepresentation, + withAudit = withAudit ) } @@ -82,20 +73,12 @@ fun composeTemporalEntitiesQueryFromPost( requestParams, contexts ).bind() + val temporalRepresentation = extractTemporalRepresentation(requestParams).bind() - val withTemporalValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), - OptionsParamValue.TEMPORAL_VALUES - ) val withAudit = hasValueInOptionsParam( Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), - OptionsParamValue.AUDIT - ) - val withAggregatedValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), - OptionsParamValue.AGGREGATED_VALUES - ) - + OptionsValue.AUDIT + ).bind() val temporalParams = mapOf( QueryParameter.TIMEREL.key to listOf(query.temporalQ?.timerel), QueryParameter.TIMEAT.key to listOf(query.temporalQ?.timeAt), @@ -109,15 +92,14 @@ fun composeTemporalEntitiesQueryFromPost( MultiValueMapAdapter(temporalParams), defaultPagination, true, - withAggregatedValues + temporalRepresentation ).bind() TemporalEntitiesQueryFromPost( entitiesQuery = entitiesQueryFromPost, temporalQuery = temporalQuery, - withTemporalValues = withTemporalValues, - withAudit = withAudit, - withAggregatedValues = withAggregatedValues + temporalRepresentation = temporalRepresentation, + withAudit = withAudit ) } @@ -126,11 +108,12 @@ fun buildTemporalQuery( params: MultiValueMap, pagination: ApplicationProperties.Pagination, inQueryEntities: Boolean = false, - withAggregatedValues: Boolean = false, + temporalRepresentation: TemporalRepresentation, ): Either { val timerelParam = params.getFirst(QueryParameter.TIMEREL.key) val timeAtParam = params.getFirst(QueryParameter.TIMEAT.key) val endTimeAtParam = params.getFirst(QueryParameter.ENDTIMEAT.key) + val withAggregatedValues = temporalRepresentation == TemporalRepresentation.AGGREGATED_VALUES val aggrPeriodDurationParam = if (withAggregatedValues) params.getFirst(QueryParameter.AGGRPERIODDURATION.key) ?: WHOLE_TIME_RANGE_DURATION @@ -206,3 +189,38 @@ fun buildTimerelAndTime( } else { "'timerel' and 'time' must be used in conjunction".left() } + +fun extractTemporalRepresentation( + queryParams: MultiValueMap +): Either = either { + val optionsParam = Optional.ofNullable(queryParams.getFirst(QueryParameter.OPTIONS.key)) + val formatParam = queryParams.getFirst(QueryParameter.FORMAT.key) + if (formatParam != null) { + return TemporalRepresentation.fromString(formatParam) + } else { + if (!optionsParam.isEmpty) { + val hasTemporal = hasValueInOptionsParam(optionsParam, OptionsValue.TEMPORAL_VALUES).bind() + val hasAggregated = hasValueInOptionsParam(optionsParam, OptionsValue.AGGREGATED_VALUES).bind() + when { + hasTemporal && hasAggregated -> + return BadRequestDataException("Only one temporal representation can be present").left() + hasTemporal -> return TemporalRepresentation.TEMPORAL_VALUES.right() + hasAggregated -> return TemporalRepresentation.AGGREGATED_VALUES.right() + else -> return TemporalRepresentation.NORMALIZED.right() + } + } else + return TemporalRepresentation.NORMALIZED.right() + } +} + +enum class TemporalRepresentation(val key: String) { + TEMPORAL_VALUES("temporalValues"), + AGGREGATED_VALUES("aggregatedValues"), + NORMALIZED("normalized"); + companion object { + fun fromString(key: String): Either = either { + TemporalRepresentation.entries.find { it.key == key } + ?: return InvalidRequestException("'$key' is not a valid temporal representation").left() + } + } +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalApiResponses.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalApiResponses.kt index 3804985e8..6047cf8e0 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalApiResponses.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalApiResponses.kt @@ -1,5 +1,6 @@ package com.egm.stellio.search.temporal.web +import arrow.core.raise.either import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.model.CompactedEntity @@ -30,8 +31,8 @@ object TemporalApiResponses { contexts: List, range: Range?, lang: String? = null, - ): ResponseEntity { - val baseRepresentation = parseRepresentations(requestParams, mediaType) + ): ResponseEntity<*> = either { + val baseRepresentation = parseRepresentations(requestParams, mediaType).bind() // this is needed for queryEntitiesViaPost where the properties are not in the query parameters val representation = lang?.let { baseRepresentation.copy(languageFilter = it, timeproperty = query.temporalQuery.timeproperty.propertyName) @@ -48,7 +49,7 @@ object TemporalApiResponses { contexts ) - return if (range == null) + if (range == null) successResponse else ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).apply { this.headers( @@ -59,7 +60,10 @@ object TemporalApiResponses { getHeaderRange(range, query.temporalQuery) ) }.body(serializeObject(entities.toFinalRepresentation(representation))) - } + }.fold( + { it.toErrorResponse() }, + { it } + ) fun buildEntityTemporalResponse( mediaType: MediaType, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt index 2b176ef34..e804bd922 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandler.kt @@ -141,11 +141,11 @@ class TemporalEntityHandler( @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.OPTIONS, QP.FORMAT, 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.TIMEPROPERTY, QP.TIMEREL, QP.TIMEAT, QP.ENDTIMEAT, QP.LASTN, QP.LANG, QP.AGGRMETHODS, QP.AGGRPERIODDURATION, QP.SCOPEQ, QP.DATASET_ID ], - notImplemented = [QP.FORMAT, QP.LOCAL, QP.VIA, QP.PICK, QP.OMIT, QP.EXPAND_VALUES, QP.CSF] + notImplemented = [QP.LOCAL, QP.VIA, QP.PICK, QP.OMIT, QP.EXPAND_VALUES, QP.CSF] ) @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { @@ -187,10 +187,10 @@ class TemporalEntityHandler( @PathVariable entityId: URI, @AllowedParameters( implemented = [ - QP.OPTIONS, QP.ATTRS, QP.TIMEPROPERTY, QP.TIMEREL, QP.TIMEAT, QP.ENDTIMEAT, QP.LASTN, + QP.OPTIONS, QP.FORMAT, QP.ATTRS, QP.TIMEPROPERTY, QP.TIMEREL, QP.TIMEAT, QP.ENDTIMEAT, QP.LASTN, QP.LANG, QP.AGGRMETHODS, QP.AGGRPERIODDURATION, QP.DATASET_ID ], - notImplemented = [QP.FORMAT, QP.LOCAL, QP.VIA, QP.PICK, QP.OMIT] + notImplemented = [QP.LOCAL, QP.VIA, QP.PICK, QP.OMIT] ) @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { @@ -209,7 +209,7 @@ class TemporalEntityHandler( val compactedEntity = compactEntity(temporalEntity, contexts) - val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType).bind() buildEntityTemporalResponse(mediaType, contexts, temporalEntitiesQuery, range) .body(serializeObject(compactedEntity.toFinalRepresentation(ngsiLdDataRepresentation))) }.fold( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt index b4e467a5a..27370df17 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtilsTests.kt @@ -183,7 +183,7 @@ class EntitiesQueryUtilsTests { requestParams.add("count", "true") requestParams.add("offset", "1") requestParams.add("limit", "10") - requestParams.add("options", "keyValues") + requestParams.add("format", "keyValues") requestParams.add("containedBy", "urn:ngsi-ld:Beekeper:A,urn:ngsi-ld:Beekeeper:B") requestParams.add("join", "inline") requestParams.add("joinLevel", "1") diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt index 3b89c8169..24d07d80d 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/web/EntityHandlerTests.kt @@ -478,6 +478,47 @@ class EntityHandlerTests { ) } + @Test + fun `get entity by id should correctly return the representation asked in the format parameter`() { + initializeRetrieveEntityMocks() + coEvery { entityQueryService.queryEntity(any(), MOCK_USER_SUB) } returns ExpandedEntity( + mapOf( + "@id" to beehiveId.toString(), + "@type" to listOf("Beehive"), + "https://uri.etsi.org/ngsi-ld/default-context/prop1" to mapOf( + JSONLD_TYPE to NGSILD_PROPERTY_TYPE.uri, + NGSILD_PROPERTY_VALUE to mapOf( + JSONLD_VALUE to "some value" + ) + ), + "https://uri.etsi.org/ngsi-ld/default-context/rel1" to mapOf( + JSONLD_TYPE to NGSILD_RELATIONSHIP_TYPE.uri, + NGSILD_RELATIONSHIP_OBJECT to mapOf( + JSONLD_ID to "urn:ngsi-ld:Entity:1234" + ) + ) + ) + ).right() + + webClient.get() + .uri("/ngsi-ld/v1/entities/$beehiveId?format=keyValues&options=normalized") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .exchange() + .expectStatus().isOk + .expectBody() + .json( + """ + { + "id": "$beehiveId", + "type": "Beehive", + "prop1": "some value", + "rel1": "urn:ngsi-ld:Entity:1234", + "@context": "${applicationProperties.contexts.core}" + } + """.trimIndent() + ) + } + @Test fun `get entity by id should return 404 if the entity has none of the requested attributes`() { initializeRetrieveEntityMocks() diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt index 24bc39f48..a607428c8 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/scope/ScopeServiceTests.kt @@ -12,6 +12,7 @@ import com.egm.stellio.search.support.buildDefaultTestTemporalQuery import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty import com.egm.stellio.search.temporal.model.TemporalEntitiesQueryFromGet import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.model.getScopes import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS @@ -154,9 +155,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { contexts = APIC_COMPOUND_CONTEXTS ), buildDefaultTestTemporalQuery(timeproperty = TemporalProperty.MODIFIED_AT), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ) ).shouldSucceedAndResult() @@ -184,9 +184,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { timerel = TemporalQuery.Timerel.BEFORE, timeAt = ngsiLdDateTime() ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ) ).shouldSucceedAndResult() @@ -216,9 +215,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { aggrMethods = listOf(TemporalQuery.Aggregate.SUM), aggrPeriodDuration = "PT1S" ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ), ngsiLdDateTime().minusHours(1) ).shouldSucceedAndResult() @@ -250,9 +248,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { aggrMethods = listOf(TemporalQuery.Aggregate.SUM), aggrPeriodDuration = "PT0S" ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ), ngsiLdDateTime().minusHours(1) ).shouldSucceedAndResult() @@ -283,9 +280,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { aggrMethods = listOf(TemporalQuery.Aggregate.SUM), aggrPeriodDuration = "PT0S" ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ), ngsiLdDateTime().minusHours(1) ).shouldSucceedAndResult() @@ -319,9 +315,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { instanceLimit = 1, lastN = 1 ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ), ngsiLdDateTime().minusHours(1) ).shouldSucceedAndResult() @@ -365,9 +360,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { timeAt = ZonedDateTime.parse("2024-08-13T00:00:00Z"), instanceLimit = 5 ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ), ngsiLdDateTime().minusHours(1) ).shouldSucceedWith { @@ -413,9 +407,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { endTimeAt = ZonedDateTime.parse("2024-08-15T00:00:00Z"), instanceLimit = 5 ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ), ngsiLdDateTime().minusHours(1) ).shouldSucceedWith { @@ -447,9 +440,8 @@ class ScopeServiceTests : WithTimescaleContainer, WithKafkaContainer() { contexts = APIC_COMPOUND_CONTEXTS ), buildDefaultTestTemporalQuery(), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ) ).shouldSucceedAndResult() assertTrue(scopeHistoryEntries.isEmpty()) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt index 94df0ba54..cd1703094 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/scope/TemporalScopeBuilderTests.kt @@ -6,6 +6,7 @@ import com.egm.stellio.search.support.gimmeEntityPayload import com.egm.stellio.search.temporal.model.AttributeInstance.TemporalProperty import com.egm.stellio.search.temporal.model.TemporalEntitiesQueryFromGet import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.assertJsonPayloadsAreEqual import com.egm.stellio.shared.util.loadSampleData @@ -73,9 +74,8 @@ class TemporalScopeBuilderTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = temporalQuery, - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ) ) @@ -111,9 +111,8 @@ class TemporalScopeBuilderTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = temporalQuery, - withTemporalValues = true, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.TEMPORAL_VALUES, + withAudit = false ) ) @@ -151,9 +150,8 @@ class TemporalScopeBuilderTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = temporalQuery, - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ) ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt b/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt index 24e0b5d71..0181e98a1 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/support/BusinessObjectsFactory.kt @@ -7,6 +7,7 @@ import com.egm.stellio.search.entity.model.Entity import com.egm.stellio.search.temporal.model.AttributeInstance import com.egm.stellio.search.temporal.model.TemporalEntitiesQueryFromGet import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.addNonReifiedTemporalProperty import com.egm.stellio.shared.model.getSingleEntry @@ -153,9 +154,8 @@ fun gimmeVocabPropertyAttributeInstance( fun gimmeTemporalEntitiesQuery( temporalQuery: TemporalQuery, - withTemporalValues: Boolean = false, - withAudit: Boolean = false, - withAggregatedValues: Boolean = false + temporalRepresentation: TemporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit: Boolean = false ): TemporalEntitiesQueryFromGet = TemporalEntitiesQueryFromGet( entitiesQuery = EntitiesQueryFromGet( @@ -163,9 +163,8 @@ fun gimmeTemporalEntitiesQuery( contexts = APIC_COMPOUND_CONTEXTS ), temporalQuery = temporalQuery, - withTemporalValues = withTemporalValues, - withAudit = withAudit, - withAggregatedValues = withAggregatedValues + temporalRepresentation = temporalRepresentation, + withAudit = withAudit ) fun buildDefaultQueryParams(): EntitiesQueryFromGet = diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AggregatedTemporalQueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AggregatedTemporalQueryServiceTests.kt index 3efe15704..3f53b35e2 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AggregatedTemporalQueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AggregatedTemporalQueryServiceTests.kt @@ -14,6 +14,7 @@ import com.egm.stellio.search.temporal.model.AttributeInstance import com.egm.stellio.search.temporal.model.AttributeInstanceResult import com.egm.stellio.search.temporal.model.TemporalEntitiesQueryFromGet import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.model.OperationNotSupportedException import com.egm.stellio.shared.util.INCOMING_PROPERTY import com.egm.stellio.shared.util.ngsiLdDateTime @@ -508,7 +509,7 @@ class AggregatedTemporalQueryServiceTests : WithTimescaleContainer, WithKafkaCon aggrPeriodDuration = aggrPeriodDuration, aggrMethods = listOfNotNull(TemporalQuery.Aggregate.forMethod(aggrMethod)) ), - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES ) private fun assertAggregatedResult( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt index 37f9eef69..02083fb4b 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/AttributeInstanceServiceTests.kt @@ -21,6 +21,7 @@ import com.egm.stellio.search.temporal.model.FullAttributeInstanceResult import com.egm.stellio.search.temporal.model.SimplifiedAttributeInstanceResult import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.search.temporal.model.TemporalQuery.Timerel +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.model.ExpandedAttributes import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.addNonReifiedTemporalProperty @@ -199,7 +200,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.minusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -224,7 +226,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timerel = Timerel.AFTER, timeAt = now.minusHours(1), timeproperty = AttributeInstance.TemporalProperty.CREATED_AT - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -249,7 +252,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timerel = Timerel.AFTER, timeAt = now.minusHours(1), timeproperty = AttributeInstance.TemporalProperty.CREATED_AT - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -274,7 +278,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timerel = Timerel.AFTER, timeAt = now.minusHours(1), timeproperty = AttributeInstance.TemporalProperty.MODIFIED_AT - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -293,7 +298,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.minusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -309,7 +315,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer } attributeInstanceService.search( - gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery()), + gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery(), TemporalRepresentation.NORMALIZED), incomingAttribute ) .shouldSucceedWith { @@ -353,7 +359,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer } attributeInstanceService.search( - gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery(), withTemporalValues = true), + gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery(), TemporalRepresentation.TEMPORAL_VALUES), attribute2 ).shouldSucceedWith { results -> assertThat(results) @@ -381,7 +387,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer aggrPeriodDuration = "P30D", aggrMethods = listOf(TemporalQuery.Aggregate.MAX) ), - withAggregatedValues = true + TemporalRepresentation.AGGREGATED_VALUES ) val origin = attributeInstanceService.selectOldestDate( @@ -409,7 +415,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timerel = Timerel.AFTER, timeAt = ZonedDateTime.parse("2022-07-01T00:00:00Z"), instanceLimit = 5 - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) @@ -435,7 +442,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = ZonedDateTime.parse("2022-07-01T00:00:00Z"), endTimeAt = ZonedDateTime.parse("2022-07-05T00:00:00Z"), instanceLimit = 5 - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) @@ -462,7 +470,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timeAt = now.minusHours(1), instanceLimit = 5, lastN = 5 - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -492,7 +501,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer aggrMethods = listOf(TemporalQuery.Aggregate.SUM), instanceLimit = 5, ), - withAggregatedValues = true + TemporalRepresentation.AGGREGATED_VALUES ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -524,7 +533,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.minusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { results -> @@ -546,7 +556,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.minusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search( temporalEntitiesQuery, @@ -567,7 +578,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.plusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { @@ -600,7 +612,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timerel = Timerel.AFTER, timeAt = now.minusHours(1) ), - withTemporalValues = true + TemporalRepresentation.TEMPORAL_VALUES ), incomingAttribute ).shouldSucceedWith { results -> @@ -808,7 +820,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = ZonedDateTime.parse("1970-01-01T00:00:00Z") - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.modifyAttributeInstance( @@ -846,7 +859,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = ZonedDateTime.parse("1970-01-01T00:00:00Z") - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.modifyAttributeInstance( @@ -885,7 +899,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = ZonedDateTime.parse("1970-01-01T00:00:00Z") - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.modifyAttributeInstance( @@ -929,7 +944,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = ZonedDateTime.parse("1970-01-01T00:00:00Z") - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.modifyAttributeInstance( @@ -968,7 +984,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer ).shouldSucceed() attributeInstanceService.search( - gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery()), + gimmeTemporalEntitiesQuery(buildDefaultTestTemporalQuery(), TemporalRepresentation.NORMALIZED), incomingAttribute ) .shouldSucceedWith { @@ -1042,7 +1058,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.minusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, incomingAttribute) .shouldSucceedWith { assertThat(it).isEmpty() } @@ -1052,7 +1069,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer timerel = Timerel.AFTER, timeAt = now.minusHours(1), timeproperty = AttributeInstance.TemporalProperty.CREATED_AT - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesAuditQuery, incomingAttribute) @@ -1078,7 +1096,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.minusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, outgoingAttribute) .shouldSucceedWith { assertThat(it).hasSize(5) } @@ -1105,7 +1124,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer buildDefaultTestTemporalQuery( timerel = Timerel.AFTER, timeAt = now.minusHours(1) - ) + ), + TemporalRepresentation.NORMALIZED ) attributeInstanceService.search(temporalEntitiesQuery, outgoingAttribute) .shouldSucceedWith { assertThat(it).hasSize(5) } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationServiceTests.kt index 2d7bf7c20..035927876 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalPaginationServiceTests.kt @@ -13,6 +13,7 @@ import com.egm.stellio.search.temporal.model.TemporalEntitiesQueryFromGet import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.search.temporal.service.TemporalPaginationService.getPaginatedAttributeWithInstancesAndRange import com.egm.stellio.search.temporal.util.AttributesWithInstances +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.INCOMING_PROPERTY @@ -138,9 +139,8 @@ class TemporalPaginationServiceTests { attrs = setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY), contexts = APIC_COMPOUND_CONTEXTS ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ) @Test @@ -282,9 +282,8 @@ class TemporalPaginationServiceTests { attrs = setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY), contexts = APIC_COMPOUND_CONTEXTS ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ) val attributesWithInstances: AttributesWithInstances = diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt index 2a3cc15d9..c34028380 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/service/TemporalQueryServiceTests.kt @@ -20,6 +20,7 @@ import com.egm.stellio.search.temporal.model.FullAttributeInstanceResult import com.egm.stellio.search.temporal.model.SimplifiedAttributeInstanceResult import com.egm.stellio.search.temporal.model.TemporalEntitiesQueryFromGet import com.egm.stellio.search.temporal.model.TemporalQuery +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIARY_TYPE @@ -91,9 +92,8 @@ class TemporalQueryServiceTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = buildDefaultTestTemporalQuery(), - withTemporalValues = true, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.TEMPORAL_VALUES, + withAudit = false ) ).fold({ assertInstanceOf(ResourceNotFoundException::class.java, it) @@ -139,9 +139,8 @@ class TemporalQueryServiceTests { paginationQuery = PaginationQuery(limit = 0, offset = 50), contexts = APIC_COMPOUND_CONTEXTS ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ) ) @@ -172,9 +171,8 @@ class TemporalQueryServiceTests { paginationQuery = PaginationQuery(limit = 0, offset = 50), contexts = APIC_COMPOUND_CONTEXTS ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ), emptyList() ) @@ -195,9 +193,8 @@ class TemporalQueryServiceTests { paginationQuery = PaginationQuery(limit = 0, offset = 50), contexts = APIC_COMPOUND_CONTEXTS ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ), emptyList() ) @@ -223,9 +220,8 @@ class TemporalQueryServiceTests { paginationQuery = PaginationQuery(limit = 0, offset = 50), contexts = APIC_COMPOUND_CONTEXTS ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ), emptyList() ) @@ -274,9 +270,8 @@ class TemporalQueryServiceTests { timerel = TemporalQuery.Timerel.BEFORE, timeAt = ZonedDateTime.parse("2019-10-17T07:31:39Z") ), - withTemporalValues = true, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.TEMPORAL_VALUES, + withAudit = false ) ) @@ -345,9 +340,8 @@ class TemporalQueryServiceTests { timeAt = ZonedDateTime.parse("2019-10-17T07:31:39Z"), aggrMethods = listOf(TemporalQuery.Aggregate.AVG) ), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ) ) .fold({ diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntitiesParameterizedSource.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntitiesParameterizedSource.kt index 2c74de4b7..f1f6f6a86 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntitiesParameterizedSource.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntitiesParameterizedSource.kt @@ -227,19 +227,19 @@ class TemporalEntitiesParameterizedSource { return Stream.of( Arguments.arguments( resultOfTwoEntitiesWithOneProperty, - false, + TemporalRepresentation.NORMALIZED, true, loadSampleData("expectations/query/two_beehives.json") ), Arguments.arguments( simplifiedResultOfTwoEntitiesWithOneProperty, - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/query/two_beehives_temporal_values.json") ), Arguments.arguments( simplifiedResultOfTwoEntitiesWithOnePropertyAndOneRelationship, - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/query/two_entities_temporal_values_property_and_relationship.json") ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilderTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilderTests.kt index 5fe3d43e3..4bf5e7473 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilderTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityBuilderTests.kt @@ -56,9 +56,8 @@ class TemporalEntityBuilderTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = buildDefaultTestTemporalQuery(), - withTemporalValues = false, - withAudit = false, - withAggregatedValues = false + temporalRepresentation = TemporalRepresentation.NORMALIZED, + withAudit = false ) ) assertJsonPayloadsAreEqual( @@ -73,7 +72,7 @@ class TemporalEntityBuilderTests { fun `it should correctly build a temporal entity`( scopeHistory: List, attributeAndResultsMap: AttributesWithInstances, - withTemporalValues: Boolean, + temporalRepresentation: TemporalRepresentation, withAudit: Boolean, expectation: String ) { @@ -89,9 +88,8 @@ class TemporalEntityBuilderTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = buildDefaultTestTemporalQuery(), - withTemporalValues, - withAudit, - false + temporalRepresentation, + withAudit ) ) assertJsonPayloadsAreEqual( @@ -105,7 +103,7 @@ class TemporalEntityBuilderTests { @MethodSource("com.egm.stellio.search.temporal.util.TemporalEntitiesParameterizedSource#rawResultsProvider") fun `it should correctly build temporal entities`( entityTemporalResults: List, - withTemporalValues: Boolean, + temporalRepresentation: TemporalRepresentation, withAudit: Boolean, expectation: String ) { @@ -114,9 +112,8 @@ class TemporalEntityBuilderTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = buildDefaultTestTemporalQuery(), - withTemporalValues, - withAudit, - false + temporalRepresentation, + withAudit ) ) assertJsonPayloadsAreEqual( @@ -193,9 +190,8 @@ class TemporalEntityBuilderTests { TemporalEntitiesQueryFromGet( entitiesQuery = buildDefaultQueryParams(), temporalQuery = temporalQuery, - withTemporalValues = false, - withAudit = false, - withAggregatedValues = true + temporalRepresentation = TemporalRepresentation.AGGREGATED_VALUES, + withAudit = false ) ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityParameterizedSource.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityParameterizedSource.kt index 133701db2..b822787ce 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityParameterizedSource.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalEntityParameterizedSource.kt @@ -69,7 +69,7 @@ class TemporalEntityParameterizedSource { ) ) ), - false, + TemporalRepresentation.NORMALIZED, false, loadSampleData("expectations/beehive_incoming_multi_instances_without_datasetId.jsonld") ) @@ -115,7 +115,7 @@ class TemporalEntityParameterizedSource { ) ) ), - false, + TemporalRepresentation.NORMALIZED, false, loadSampleData("expectations/beehive_relationship_multi_instances_without_datasetId.jsonld") ) @@ -193,7 +193,7 @@ class TemporalEntityParameterizedSource { ) ) ), - false, + TemporalRepresentation.NORMALIZED, false, loadSampleData("expectations/beehive_incoming_multi_instances.jsonld") ) @@ -237,7 +237,7 @@ class TemporalEntityParameterizedSource { ) ) ), - false, + TemporalRepresentation.NORMALIZED, false, loadSampleData("expectations/beehive_incoming_multi_instances_string_values.jsonld") ) @@ -281,7 +281,7 @@ class TemporalEntityParameterizedSource { ) ) ), - false, + TemporalRepresentation.NORMALIZED, true, loadSampleData("expectations/beehive_incoming_multi_instances_string_values_with_audit.jsonld") ) @@ -324,7 +324,7 @@ class TemporalEntityParameterizedSource { ) ) ), - false, + TemporalRepresentation.NORMALIZED, false, loadSampleData( "expectations/beehive_incoming_multi_instances_without_datasetId_string_values.jsonld" @@ -355,7 +355,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData( "expectations/beehive_incoming_multi_instances_without_datasetId_temporal_values.jsonld" @@ -407,7 +407,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/beehive_incoming_multi_instances_temporal_values.jsonld") ) @@ -437,7 +437,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/beehive_incoming_multi_instances_string_temporal_values.jsonld") ) @@ -466,7 +466,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData( "expectations/beehive_incoming_multi_instances_without_datasetId_string_temporal_values.jsonld" @@ -499,7 +499,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/beehive_relationship_multi_instances_temporal_values.jsonld") ) @@ -519,7 +519,7 @@ class TemporalEntityParameterizedSource { ) ), emptyMap>(), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/beehive_scope_multi_instances_temporal_values.jsonld") ) @@ -541,7 +541,7 @@ class TemporalEntityParameterizedSource { ) ), emptyMap>(), - false, + TemporalRepresentation.NORMALIZED, false, loadSampleData("expectations/beehive_scope_multi_instances.jsonld") ) @@ -576,7 +576,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/beehive_json_property_temporal_values.jsonld") ) @@ -625,7 +625,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/beehive_language_property_temporal_values.jsonld") ) @@ -667,7 +667,7 @@ class TemporalEntityParameterizedSource { ) ) ), - true, + TemporalRepresentation.TEMPORAL_VALUES, false, loadSampleData("expectations/beehive_vocab_property_temporal_values.jsonld") ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtilsTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtilsTests.kt index c701f8413..977aa8ebc 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtilsTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryUtilsTests.kt @@ -1,7 +1,6 @@ package com.egm.stellio.search.temporal.util import com.egm.stellio.search.common.model.Query -import com.egm.stellio.search.entity.model.EntitiesQueryFromGet import com.egm.stellio.search.entity.model.EntitiesQueryFromPost import com.egm.stellio.search.support.buildDefaultPagination import com.egm.stellio.search.support.buildDefaultTestTemporalQuery @@ -10,6 +9,7 @@ import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.EntitySelector +import com.egm.stellio.shared.model.InvalidRequestException import com.egm.stellio.shared.util.APIARY_TYPE import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.BEEHIVE_TYPE @@ -94,6 +94,67 @@ class TemporalQueryUtilsTests { } } + @Test + fun `it shouldn't validate the temporal query if both temporalValues and aggregatedValues are present`() = runTest { + val queryParams = gimmeTemporalEntitiesQueryParams() + queryParams.replace("options", listOf("aggregatedValues,temporalValues")) + queryParams.add("aggrMethods", "sum") + val pagination = mockkClass(ApplicationProperties.Pagination::class) + every { pagination.limitDefault } returns 30 + every { pagination.limitMax } returns 100 + every { pagination.temporalLimit } returns 100 + + composeTemporalEntitiesQueryFromGet( + pagination, + queryParams, + APIC_COMPOUND_CONTEXTS, + true + ).shouldFail { + assertInstanceOf(BadRequestDataException::class.java, it) + assertEquals("Only one temporal representation can be present", it.message) + } + } + + @Test + fun `it shouldn't validate the temporal query if format contains an invalid value`() = runTest { + val queryParams = gimmeTemporalEntitiesQueryParams() + queryParams.add("format", "invalid") + val pagination = mockkClass(ApplicationProperties.Pagination::class) + every { pagination.limitDefault } returns 30 + every { pagination.limitMax } returns 100 + every { pagination.temporalLimit } returns 100 + + composeTemporalEntitiesQueryFromGet( + pagination, + queryParams, + APIC_COMPOUND_CONTEXTS, + true + ).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'invalid' is not a valid temporal representation", it.message) + } + } + + @Test + fun `it shouldn't validate the temporal query if options contains an invalid value`() = runTest { + val queryParams = gimmeTemporalEntitiesQueryParams() + queryParams.replace("options", listOf("invalidOptions")) + val pagination = mockkClass(ApplicationProperties.Pagination::class) + every { pagination.limitDefault } returns 30 + every { pagination.limitMax } returns 100 + every { pagination.temporalLimit } returns 100 + + composeTemporalEntitiesQueryFromGet( + pagination, + queryParams, + APIC_COMPOUND_CONTEXTS, + true + ).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'invalidOptions' is not a valid options value", it.message) + } + } + @Test fun `it should parse a valid temporal query`() = runTest { val queryParams = gimmeTemporalEntitiesQueryParams() @@ -109,11 +170,11 @@ class TemporalQueryUtilsTests { assertEquals( setOf("urn:ngsi-ld:BeeHive:TESTC".toUri(), "urn:ngsi-ld:BeeHive:TESTB".toUri()), - (temporalEntitiesQuery.entitiesQuery as EntitiesQueryFromGet).ids + temporalEntitiesQuery.entitiesQuery.ids ) assertEquals( "$BEEHIVE_TYPE,$APIARY_TYPE", - (temporalEntitiesQuery.entitiesQuery as EntitiesQueryFromGet).typeSelection + temporalEntitiesQuery.entitiesQuery.typeSelection ) assertEquals(setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY), temporalEntitiesQuery.entitiesQuery.attrs) assertEquals( @@ -124,7 +185,7 @@ class TemporalQueryUtilsTests { ), temporalEntitiesQuery.temporalQuery ) - assertTrue(temporalEntitiesQuery.withTemporalValues) + assertTrue(temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES) assertFalse(temporalEntitiesQuery.withAudit) assertEquals(10, temporalEntitiesQuery.entitiesQuery.paginationQuery.limit) assertEquals(2, temporalEntitiesQuery.entitiesQuery.paginationQuery.offset) @@ -228,7 +289,12 @@ class TemporalQueryUtilsTests { queryParams.add("timeAt", "2019-10-17T07:31:39Z") queryParams.add("lastN", "2") - val temporalQuery = buildTemporalQuery(queryParams, buildDefaultPagination()).shouldSucceedAndResult() + val temporalQuery = buildTemporalQuery( + queryParams, + buildDefaultPagination(), + false, + TemporalRepresentation.NORMALIZED + ).shouldSucceedAndResult() assertEquals(2, temporalQuery.instanceLimit) assertEquals(2, temporalQuery.lastN) @@ -241,7 +307,12 @@ class TemporalQueryUtilsTests { queryParams.add("timeAt", "2019-10-17T07:31:39Z") queryParams.add("lastN", "A") val pagination = buildDefaultPagination() - val temporalQuery = buildTemporalQuery(queryParams, pagination).shouldSucceedAndResult() + val temporalQuery = buildTemporalQuery( + queryParams, + pagination, + false, + TemporalRepresentation.NORMALIZED + ).shouldSucceedAndResult() assertEquals(pagination.temporalLimit, temporalQuery.instanceLimit) assertNull(temporalQuery.lastN) @@ -255,7 +326,12 @@ class TemporalQueryUtilsTests { queryParams.add("lastN", "-2") val pagination = buildDefaultPagination() - val temporalQuery = buildTemporalQuery(queryParams, pagination).shouldSucceedAndResult() + val temporalQuery = buildTemporalQuery( + queryParams, + pagination, + false, + TemporalRepresentation.NORMALIZED + ).shouldSucceedAndResult() assertEquals(pagination.temporalLimit, temporalQuery.instanceLimit) assertNull(temporalQuery.lastN) @@ -265,7 +341,12 @@ class TemporalQueryUtilsTests { fun `it should treat time and timerel properties as optional in a temporal query`() = runTest { val queryParams = LinkedMultiValueMap() - val temporalQuery = buildTemporalQuery(queryParams, buildDefaultPagination()).shouldSucceedAndResult() + val temporalQuery = buildTemporalQuery( + queryParams, + buildDefaultPagination(), + false, + TemporalRepresentation.NORMALIZED + ).shouldSucceedAndResult() assertNull(temporalQuery.timeAt) assertNull(temporalQuery.timerel) @@ -276,7 +357,12 @@ class TemporalQueryUtilsTests { val queryParams = LinkedMultiValueMap() queryParams.add("timeproperty", "createdAt") - val temporalQuery = buildTemporalQuery(queryParams, buildDefaultPagination()).shouldSucceedAndResult() + val temporalQuery = buildTemporalQuery( + queryParams, + buildDefaultPagination(), + false, + TemporalRepresentation.NORMALIZED + ).shouldSucceedAndResult() assertEquals(AttributeInstance.TemporalProperty.CREATED_AT, temporalQuery.timeproperty) } @@ -285,7 +371,12 @@ class TemporalQueryUtilsTests { fun `it should set timeproperty to observedAt if no value is provided in query parameters`() = runTest { val queryParams = LinkedMultiValueMap() - val temporalQuery = buildTemporalQuery(queryParams, buildDefaultPagination()).shouldSucceedAndResult() + val temporalQuery = buildTemporalQuery( + queryParams, + buildDefaultPagination(), + false, + TemporalRepresentation.NORMALIZED + ).shouldSucceedAndResult() assertEquals(AttributeInstance.TemporalProperty.OBSERVED_AT, temporalQuery.timeproperty) } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTests.kt index 73d2f7541..ed1d3c901 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityHandlerTests.kt @@ -8,6 +8,7 @@ import com.egm.stellio.search.entity.model.EntitiesQueryFromGet import com.egm.stellio.search.support.buildDefaultTestTemporalQuery import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.search.temporal.service.TemporalService.CreateOrUpdateResult +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.BadRequestDataException @@ -425,6 +426,69 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { ) } + @Test + fun `it should raise a 400 if temporalValues and aggregatedValues exist in options query param`() { + webClient.get() + .uri( + "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + + "timerel=after&timeAt=2020-01-31T07:31:39Z&aggrPeriodDuration=P1D&" + + "options=aggregatedValues,temporalValues" + ) + .exchange() + .expectStatus().isBadRequest + .expectBody().json( + """ + { + "type": "https://uri.etsi.org/ngsi-ld/errors/BadRequestData", + "title": "Only one temporal representation can be present", + "detail": "$DEFAULT_DETAIL" + } + """ + ) + } + + @Test + fun `it should raise a 400 if format query param has an invalid value`() { + webClient.get() + .uri( + "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + + "timerel=after&timeAt=2020-01-31T07:31:39Z&" + + "format=invalid" + ) + .exchange() + .expectStatus().isBadRequest + .expectBody().json( + """ + { + "type": "https://uri.etsi.org/ngsi-ld/errors/InvalidRequest", + "title": "'invalid' is not a valid temporal representation", + "detail": "$DEFAULT_DETAIL" + } + """ + ) + } + + @Test + fun `it should raise a 400 if options query param has an invalid value`() { + webClient.get() + .uri( + "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + + "timerel=after&timeAt=2020-01-31T07:31:39Z&" + + "options=invalidOptions" + ) + .exchange() + .expectStatus().isBadRequest + .expectBody().json( + """ + { + "type": "https://uri.etsi.org/ngsi-ld/errors/InvalidRequest", + "title": "'invalidOptions' is not a valid options value", + "detail": "$DEFAULT_DETAIL" + } + """ + ) + } + @Test fun `it should return a 404 if temporal entity attribute does not exist`() { coEvery { @@ -473,7 +537,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { temporalEntitiesQuery.temporalQuery.timeAt!!.isEqual( ZonedDateTime.parse("2019-10-17T07:31:39Z") ) && - !temporalEntitiesQuery.withTemporalValues && + temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.NORMALIZED && !temporalEntitiesQuery.withAudit }, eq(sub.value) @@ -604,7 +668,7 @@ class TemporalEntityHandlerTests : TemporalEntityHandlerTestCommon() { entitiesQueryFromGet.ids.isEmpty() && entitiesQueryFromGet.typeSelection == BEEHIVE_TYPE && temporalEntitiesQuery.temporalQuery == temporalQuery && - !temporalEntitiesQuery.withTemporalValues + temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.NORMALIZED }, any() ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandlerTests.kt index 7f4ff8e9b..5f04f17d2 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandlerTests.kt @@ -6,6 +6,7 @@ import com.egm.stellio.search.entity.model.EntitiesQueryFromPost import com.egm.stellio.search.support.buildDefaultTestTemporalQuery import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.search.temporal.service.TemporalQueryService +import com.egm.stellio.search.temporal.util.TemporalRepresentation import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.DEFAULT_DETAIL import com.egm.stellio.shared.util.APIARY_COMPACT_TYPE @@ -101,7 +102,7 @@ class TemporalEntityOperationsHandlerTests { entitiesQueryFromPost.entitySelectors!![0].typeSelection == "$BEEHIVE_TYPE,$APIARY_TYPE" && temporalEntitiesQuery.entitiesQuery.attrs == setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY) && temporalEntitiesQuery.temporalQuery == temporalQuery && - temporalEntitiesQuery.withTemporalValues + temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES }, any() ) @@ -155,7 +156,7 @@ class TemporalEntityOperationsHandlerTests { temporalEntitiesQuery.entitiesQuery.attrs == setOf(INCOMING_PROPERTY, OUTGOING_PROPERTY) && temporalEntitiesQuery.entitiesQuery.paginationQuery.count && temporalEntitiesQuery.temporalQuery == temporalQuery && - temporalEntitiesQuery.withTemporalValues + temporalEntitiesQuery.temporalRepresentation == TemporalRepresentation.TEMPORAL_VALUES }, any() ) 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 index ba2dbcfca..8ae1a3363 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentation.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentation.kt @@ -1,5 +1,10 @@ package com.egm.stellio.shared.model +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.egm.stellio.shared.queryparameter.FormatValue import com.egm.stellio.shared.queryparameter.OptionsValue import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.GEO_JSON_MEDIA_TYPE @@ -26,11 +31,26 @@ data class NgsiLdDataRepresentation( fun parseRepresentations( queryParams: MultiValueMap, acceptMediaType: MediaType - ): NgsiLdDataRepresentation { + ): Either = either { val optionsParam = queryParams.getOrDefault(QueryParameter.OPTIONS.key, emptyList()) + .flatMap { it.split(",") } + val formatParam = queryParams.getFirst(QueryParameter.FORMAT.key) + if (formatParam != null && FormatValue.fromString(formatParam) == null) { + return InvalidRequestException("'$formatParam' is not a valid format value").left() + } + + optionsParam.forEach { option -> + OptionsValue.fromString(option).bind() + } + val attributeRepresentation = when { + formatParam == FormatValue.KEY_VALUES.value || + formatParam == FormatValue.SIMPLIFIED.value -> AttributeRepresentation.SIMPLIFIED + formatParam == FormatValue.NORMALIZED.value -> AttributeRepresentation.NORMALIZED + optionsParam.contains(FormatValue.KEY_VALUES.value) || + optionsParam.contains(FormatValue.SIMPLIFIED.value) -> AttributeRepresentation.SIMPLIFIED + else -> AttributeRepresentation.NORMALIZED + } val includeSysAttrs = optionsParam.contains(OptionsValue.SYS_ATTRS.value) - val attributeRepresentation = optionsParam.contains(OptionsValue.KEY_VALUES.value) - .let { if (it) AttributeRepresentation.SIMPLIFIED else AttributeRepresentation.NORMALIZED } val languageFilter = queryParams.getFirst(QueryParameter.LANG.key) val entityRepresentation = EntityRepresentation.forMediaType(acceptMediaType) val geometryProperty = @@ -46,7 +66,7 @@ data class NgsiLdDataRepresentation( languageFilter, geometryProperty, timeproperty - ) + ).right() } } } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/FormatValue.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/FormatValue.kt new file mode 100644 index 000000000..0565a3c61 --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/FormatValue.kt @@ -0,0 +1,13 @@ +package com.egm.stellio.shared.queryparameter + +enum class FormatValue(val value: String) { + KEY_VALUES("keyValues"), + SIMPLIFIED("simplified"), + NORMALIZED("normalized"), + TEMPORAL_VALUES("temporalValues"), + AGGREGATED_VALUES("aggregatedValues"); + companion object { + fun fromString(key: String): FormatValue? = + FormatValue.entries.find { it.value == key } + } +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/OptionsValue.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/OptionsValue.kt index 05072d108..25fa030b7 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/OptionsValue.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/OptionsValue.kt @@ -1,9 +1,26 @@ package com.egm.stellio.shared.queryparameter +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.InvalidRequestException + enum class OptionsValue(val value: String) { SYS_ATTRS("sysAttrs"), - KEY_VALUES("keyValues"), NO_OVERWRITE("noOverwrite"), UPDATE_MODE("update"), - REPLACE_MODE("replace") + REPLACE_MODE("replace"), + TEMPORAL_VALUES("temporalValues"), + AGGREGATED_VALUES("aggregatedValues"), + AUDIT("audit"), + NORMALIZED("normalized"), + KEY_VALUES("keyValues"), + SIMPLIFIED("simplified"); + companion object { + fun fromString(key: String): Either = either { + OptionsValue.entries.find { it.value == key } + ?: return InvalidRequestException("'$key' is not a valid options value").left() + } + } } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/QueryParameter.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/QueryParameter.kt index 3762001e6..f8ac16d8e 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/QueryParameter.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/QueryParameter.kt @@ -18,6 +18,7 @@ enum class QueryParameter( JOIN("join"), JOIN_LEVEL("joinLevel"), OPTIONS("options"), + FORMAT("format"), OBSERVED_AT("observedAt"), // geoQuery @@ -46,7 +47,6 @@ enum class QueryParameter( DELETE_ALL("deleteAll"), // not implemented yet - FORMAT("format"), PICK("pick"), OMIT("omit"), EXPAND_VALUES("expandValues"), 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 8f8c8176d..37c48968f 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 @@ -11,6 +11,7 @@ import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.CompactedEntity import com.egm.stellio.shared.model.EntityTypeSelection import com.egm.stellio.shared.model.NotAcceptableException +import com.egm.stellio.shared.queryparameter.OptionsValue import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_PROPERTY import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap @@ -168,17 +169,19 @@ internal fun canExpandJsonLdKeyFromCore(contexts: List): Boolean { return expandedType == NGSILD_DATASET_ID_PROPERTY } -enum class OptionsParamValue(val value: String) { - TEMPORAL_VALUES("temporalValues"), - AUDIT("audit"), - AGGREGATED_VALUES("aggregatedValues") -} - -fun hasValueInOptionsParam(options: Optional, optionValue: OptionsParamValue): Boolean = - options +fun hasValueInOptionsParam( + queryParam: Optional, + optionsParamValue: OptionsValue +): Either = either { + val optionsValue = queryParam .map { it.split(",") } - .filter { it.any { option -> option == optionValue.value } } - .isPresent + .orElse(emptyList()) + + optionsValue.forEach { option -> + OptionsValue.fromString(option).bind() + } + optionsValue.any { option -> option == optionsParamValue.value } +} fun parseQueryParameter(queryParam: String?): Set = queryParam diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentationTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentationTests.kt new file mode 100644 index 000000000..bb29c9ddd --- /dev/null +++ b/shared/src/test/kotlin/com/egm/stellio/shared/model/NgsiLdDataRepresentationTests.kt @@ -0,0 +1,110 @@ +package com.egm.stellio.shared.model + +import com.egm.stellio.shared.model.NgsiLdDataRepresentation.Companion.parseRepresentations +import com.egm.stellio.shared.util.shouldFail +import com.egm.stellio.shared.util.shouldSucceedAndResult +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import org.springframework.util.LinkedMultiValueMap + +@ActiveProfiles("test") +class NgsiLdDataRepresentationTests { + + @Test + fun `it should return the attribute representation in the format query param when options exist`() { + val queryParams = LinkedMultiValueMap() + queryParams.add("timerel", "after") + queryParams.add("timeAt", "2025-01-03T07:45:24Z") + queryParams.add("options", "simplified") + queryParams.add("format", "normalized") + + val parsedRepresentation = parseRepresentations(queryParams, MediaType.APPLICATION_JSON) + .shouldSucceedAndResult() + + assertEquals(AttributeRepresentation.NORMALIZED, parsedRepresentation.attributeRepresentation) + } + + @Test + fun `it should return the attribute representation in the options query param when no format is given`() { + val queryParams = LinkedMultiValueMap() + queryParams.add("timerel", "after") + queryParams.add("timeAt", "2025-01-03T07:45:24Z") + queryParams.add("options", "simplified") + + val parsedRepresentation = parseRepresentations(queryParams, MediaType.APPLICATION_JSON) + .shouldSucceedAndResult() + + assertEquals(AttributeRepresentation.SIMPLIFIED, parsedRepresentation.attributeRepresentation) + } + + @Test + fun `it should correctly parse the options query param when it contains more than one value`() { + val queryParams = LinkedMultiValueMap() + queryParams.add("timerel", "after") + queryParams.add("timeAt", "2025-01-03T07:45:24Z") + queryParams.add("options", "simplified") + queryParams.add("options", "sysAttrs,audit") + val parsedRepresentation = parseRepresentations(queryParams, MediaType.APPLICATION_JSON) + .shouldSucceedAndResult() + + assertEquals(AttributeRepresentation.SIMPLIFIED, parsedRepresentation.attributeRepresentation) + assertTrue(parsedRepresentation.includeSysAttrs) + } + + @Test + fun `it should return the attribute representation in the first format query param`() { + val queryParams = LinkedMultiValueMap() + queryParams.add("timerel", "after") + queryParams.add("timeAt", "2025-01-03T07:45:24Z") + queryParams.add("format", "simplified") + queryParams.add("format", "normalized") + + val parsedRepresentation = parseRepresentations(queryParams, MediaType.APPLICATION_JSON) + .shouldSucceedAndResult() + + assertEquals(AttributeRepresentation.SIMPLIFIED, parsedRepresentation.attributeRepresentation) + } + + @Test + fun `it should return an exception if format query param is invalid`() { + val queryParams = LinkedMultiValueMap() + queryParams.add("timerel", "after") + queryParams.add("timeAt", "2025-01-03T07:45:24Z") + queryParams.add("format", "invalid") + + parseRepresentations(queryParams, MediaType.APPLICATION_JSON).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'invalid' is not a valid format value", it.message) + } + } + + @Test + fun `it should return an exception if options query param is invalid`() { + val queryParams = LinkedMultiValueMap() + queryParams.add("timerel", "after") + queryParams.add("timeAt", "2025-01-03T07:45:24Z") + queryParams.add("options", "invalidOptions") + + parseRepresentations(queryParams, MediaType.APPLICATION_JSON).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'invalidOptions' is not a valid options value", it.message) + } + } + + @Test + fun `it should return an exception if at least one value in options query param is invalid`() { + val queryParams = LinkedMultiValueMap() + queryParams.add("timerel", "after") + queryParams.add("timeAt", "2025-01-03T07:45:24Z") + queryParams.add("options", "simplified,invalidOptions") + + parseRepresentations(queryParams, MediaType.APPLICATION_JSON).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'invalidOptions' is not a valid options value", it.message) + } + } +} diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/util/ApiUtilsTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/util/ApiUtilsTests.kt index 00a9da49a..fce6ec97c 100644 --- a/shared/src/test/kotlin/com/egm/stellio/shared/util/ApiUtilsTests.kt +++ b/shared/src/test/kotlin/com/egm/stellio/shared/util/ApiUtilsTests.kt @@ -2,7 +2,8 @@ package com.egm.stellio.shared.util import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.BadRequestDataException -import com.egm.stellio.shared.util.OptionsParamValue.TEMPORAL_VALUES +import com.egm.stellio.shared.model.InvalidRequestException +import com.egm.stellio.shared.queryparameter.OptionsValue.TEMPORAL_VALUES import com.egm.stellio.shared.web.CustomWebFilter import io.mockk.every import io.mockk.mockk @@ -33,27 +34,38 @@ class ApiUtilsTests { @Test fun `it should not find a value if there is no options query param`() { - assertFalse(hasValueInOptionsParam(Optional.empty(), TEMPORAL_VALUES)) + val result = hasValueInOptionsParam(Optional.empty(), TEMPORAL_VALUES).shouldSucceedAndResult() + assertFalse(result) } @Test - fun `it should not find a value if it is not in a single value options query param`() { - assertFalse(hasValueInOptionsParam(Optional.of("one"), TEMPORAL_VALUES)) + fun `it should return an exception if it is given an invalid options query param`() { + hasValueInOptionsParam(Optional.of("one"), TEMPORAL_VALUES).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'one' is not a valid options value", it.message) + } } @Test - fun `it should not find a value if it is not in a multi value options query param`() { - assertFalse(hasValueInOptionsParam(Optional.of("one,two"), TEMPORAL_VALUES)) + fun `it should return an exception if it is given an invalid multi value options query param`() { + hasValueInOptionsParam(Optional.of("one,two"), TEMPORAL_VALUES).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'one' is not a valid options value", it.message) + } } @Test fun `it should find a value if it is in a single value options query param`() { - assertTrue(hasValueInOptionsParam(Optional.of("temporalValues"), TEMPORAL_VALUES)) + val result = hasValueInOptionsParam(Optional.of("temporalValues"), TEMPORAL_VALUES).shouldSucceedAndResult() + assertTrue(result) } @Test - fun `it should find a value if it is in a multi value options query param`() { - assertTrue(hasValueInOptionsParam(Optional.of("one,temporalValues"), TEMPORAL_VALUES)) + fun `it should return an exception if it is given at least one invalid value in options query param`() { + hasValueInOptionsParam(Optional.of("one,temporalValues"), TEMPORAL_VALUES).shouldFail { + assertInstanceOf(InvalidRequestException::class.java, it) + assertEquals("'one' is not a valid options value", it.message) + } } @Test