diff --git a/build.gradle.kts b/build.gradle.kts index 1fbf1efaf..98e867d8a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,6 +58,7 @@ subprojects { implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-validation") // it provides support for JWT decoding and verification implementation("org.springframework.security:spring-security-oauth2-jose") diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index e29567ca7..3d25511d4 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -733,7 +733,7 @@ style: active: true UnusedParameter: active: true - allowedNames: 'ignored|expected' + allowedNames: 'ignored|expected|queryParams' UnusedPrivateClass: active: true UnusedPrivateMember: diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml index 4ccc60237..3314d7610 100644 --- a/search-service/config/detekt/baseline.xml +++ b/search-service/config/detekt/baseline.xml @@ -9,7 +9,7 @@ Filename:V0_29__JsonLd_migration.kt$db.migration.V0_29__JsonLd_migration.kt LongMethod:AttributeInstanceService.kt$AttributeInstanceService$@Transactional suspend fun create(attributeInstance: AttributeInstance): Either<APIException, Unit> LongMethod:EnabledAuthorizationServiceTests.kt$EnabledAuthorizationServiceTests$@Test fun `it should return serialized access control entities with other rigths if user is owner`() - LongMethod:EntityAccessControlHandler.kt$EntityAccessControlHandler$@PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun addRightsOnEntities( @RequestHeader httpHeaders: HttpHeaders, @PathVariable subjectId: String, @RequestBody requestBody: Mono<String> ): ResponseEntity<*> + 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: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> @@ -27,6 +27,7 @@ LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, createdAt: ZonedDateTime, observedAt: ZonedDateTime?, sub: Sub? ) LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List<NgsiLdAttribute>, expandedAttributes: ExpandedAttributes, disallowOverwrite: Boolean, createdAt: ZonedDateTime, sub: Sub? ) LongParameterList:EntityEventService.kt$EntityEventService$( updatedDetails: UpdatedDetails, sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair<List<ExpandedTerm>, String>, serializedAttribute: Pair<ExpandedTerm, String>, overwrite: Boolean ) + LongParameterList:TemporalEntityHandler.kt$TemporalEntityHandler$( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, @PathVariable instanceId: URI, @RequestBody requestBody: Mono<String>, @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) @RequestParam queryParams: MultiValueMap<String, String> ) LongParameterList:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$( entityId: URI, attributeName: ExpandedTerm, datasetId: URI?, attributePayload: ExpandedAttributeInstance, ngsiLdAttributeInstance: NgsiLdAttributeInstance, defaultCreatedAt: ZonedDateTime ) NestedBlockDepth:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context) SwallowedException:TemporalQueryUtils.kt$e: IllegalArgumentException diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt index c4abf33bf..e85454db8 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsService.kt @@ -23,8 +23,8 @@ import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.EntityTypeSelection import com.egm.stellio.shared.model.NgsiLdAttribute -import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.model.ResourceNotFoundException +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.AccessRight import com.egm.stellio.shared.util.AccessRight.CAN_ADMIN import com.egm.stellio.shared.util.AccessRight.CAN_READ 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 09d307d2f..6bfaa771c 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 @@ -1,5 +1,6 @@ package com.egm.stellio.search.authorization.web +import arrow.core.computations.ResultEffect.bind import arrow.core.left import arrow.core.raise.either import com.egm.stellio.search.authorization.service.AuthorizationService @@ -12,10 +13,13 @@ import com.egm.stellio.search.entity.util.composeEntitiesQueryFromGet import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.BadRequestDataException +import com.egm.stellio.shared.model.NgsiLdDataRepresentation.Companion.parseRepresentations import com.egm.stellio.shared.model.NgsiLdRelationship import com.egm.stellio.shared.model.toFinalRepresentation import com.egm.stellio.shared.model.toNgsiLdAttribute import com.egm.stellio.shared.model.toNgsiLdAttributes +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.QP import com.egm.stellio.shared.util.AccessRight import com.egm.stellio.shared.util.AuthContextModel.ALL_ASSIGNABLE_IAM_RIGHTS import com.egm.stellio.shared.util.AuthContextModel.ALL_IAM_RIGHTS @@ -34,7 +38,6 @@ import com.egm.stellio.shared.util.checkAndGetContext import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getAuthzContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext -import com.egm.stellio.shared.util.parseRepresentations import com.egm.stellio.shared.util.replaceDefaultContextToAuthzContext import com.egm.stellio.shared.web.BaseHandler import kotlinx.coroutines.reactive.awaitFirst @@ -43,6 +46,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -55,6 +59,7 @@ import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.net.URI +@Validated @RestController @RequestMapping("/ngsi-ld/v1/entityAccessControl") class EntityAccessControlHandler( @@ -66,7 +71,8 @@ class EntityAccessControlHandler( @GetMapping("/entities", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getAuthorizedEntities( @RequestHeader httpHeaders: HttpHeaders, - @RequestParam params: MultiValueMap + @AllowedParameters(implemented = [QP.ID, QP.TYPE, QP.ATTRS, QP.COUNT, QP.OFFSET, QP.LIMIT]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -75,7 +81,7 @@ class EntityAccessControlHandler( val entitiesQuery = composeEntitiesQueryFromGet( applicationProperties.pagination, - params, + queryParams, contexts ).bind() @@ -96,13 +102,13 @@ class EntityAccessControlHandler( val compactedEntities = compactEntities(entities, contexts) - val ngsiLdDataRepresentation = parseRepresentations(params, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) buildQueryResponse( compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), count, "/ngsi-ld/v1/entityAccessControl/entities", entitiesQuery.paginationQuery, - params, + queryParams, mediaType, contexts ) @@ -114,6 +120,7 @@ class EntityAccessControlHandler( @GetMapping("/groups", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getGroupsMemberships( @RequestHeader httpHeaders: HttpHeaders, + @AllowedParameters(implemented = [QP.COUNT, QP.OFFSET, QP.LIMIT]) @RequestParam params: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -158,6 +165,7 @@ class EntityAccessControlHandler( @GetMapping("/users", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getUsers( @RequestHeader httpHeaders: HttpHeaders, + @AllowedParameters(implemented = [QP.COUNT, QP.OFFSET, QP.LIMIT]) @RequestParam params: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -204,7 +212,9 @@ class EntityAccessControlHandler( suspend fun addRightsOnEntities( @RequestHeader httpHeaders: HttpHeaders, @PathVariable subjectId: String, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -282,7 +292,9 @@ class EntityAccessControlHandler( @DeleteMapping("/{subjectId}/attrs/{entityId}") suspend fun removeRightsOnEntity( @PathVariable subjectId: String, - @PathVariable entityId: URI + @PathVariable entityId: URI, + @AllowedParameters + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -305,7 +317,9 @@ class EntityAccessControlHandler( suspend fun updateSpecificAccessPolicy( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -331,7 +345,9 @@ class EntityAccessControlHandler( @DeleteMapping("/{entityId}/attrs/specificAccessPolicy") suspend fun deleteSpecificAccessPolicy( - @PathVariable entityId: URI + @PathVariable entityId: URI, + @AllowedParameters + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/csr/service/ContextSourceCaller.kt b/search-service/src/main/kotlin/com/egm/stellio/search/csr/service/ContextSourceCaller.kt index 75e6e1c43..c296b972d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/csr/service/ContextSourceCaller.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/csr/service/ContextSourceCaller.kt @@ -10,9 +10,7 @@ import com.egm.stellio.search.csr.model.MiscellaneousWarning import com.egm.stellio.search.csr.model.NGSILDWarning import com.egm.stellio.search.csr.model.RevalidationFailedWarning import com.egm.stellio.shared.model.CompactedEntity -import com.egm.stellio.shared.util.QUERY_PARAM_GEOMETRY_PROPERTY -import com.egm.stellio.shared.util.QUERY_PARAM_LANG -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS +import com.egm.stellio.shared.queryparameter.QueryParameter import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.core.codec.DecodingException @@ -38,9 +36,9 @@ object ContextSourceCaller { val uri = URI("${csr.endpoint}$path") val queryParams = CollectionUtils.toMultiValueMap(params.toMutableMap()) - queryParams.remove(QUERY_PARAM_GEOMETRY_PROPERTY) - queryParams.remove(QUERY_PARAM_OPTIONS) // only normalized request - queryParams.remove(QUERY_PARAM_LANG) + queryParams.remove(QueryParameter.GEOMETRY_PROPERTY.key) + queryParams.remove(QueryParameter.OPTIONS.key) // only normalized request + queryParams.remove(QueryParameter.LANG.key) val request = WebClient.create() .method(HttpMethod.GET) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt index e9dc6cb8c..4fdd6f56b 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/csr/web/ContextSourceRegistrationHandler.kt @@ -13,17 +13,19 @@ import com.egm.stellio.search.csr.service.ContextSourceRegistrationService import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.AccessDeniedException +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.OptionsValue +import com.egm.stellio.shared.queryparameter.PaginationQuery.Companion.parsePaginationParameters +import com.egm.stellio.shared.queryparameter.QP +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS_SYSATTRS_VALUE import com.egm.stellio.shared.util.Sub import com.egm.stellio.shared.util.buildQueryResponse import com.egm.stellio.shared.util.checkAndGetContext import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext -import com.egm.stellio.shared.util.parsePaginationParameters import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders import com.egm.stellio.shared.web.BaseHandler import kotlinx.coroutines.reactive.awaitFirst @@ -32,6 +34,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -46,6 +49,7 @@ import java.net.URI @RestController @RequestMapping("/ngsi-ld/v1/csourceRegistrations") +@Validated class ContextSourceRegistrationHandler( private val applicationProperties: ApplicationProperties, private val contextSourceRegistrationService: ContextSourceRegistrationService @@ -81,16 +85,25 @@ class ContextSourceRegistrationHandler( @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun get( @RequestHeader httpHeaders: HttpHeaders, - @RequestParam params: MultiValueMap + @AllowedParameters( + implemented = [QP.OPTIONS, QP.COUNT, QP.OFFSET, QP.LIMIT], + notImplemented = [ + QP.ID, QP.TYPE, QP.ID_PATTERN, QP.ATTRS, QP.Q, QP.CSF, + QP.GEOMETRY, QP.GEOREL, QP.COORDINATES, QP.GEOPROPERTY, + QP.TIMEPROPERTY, QP.TIMEREL, QP.TIMEAT, QP.ENDTIMEAT, + QP.GEOMETRY_PROPERTY, QP.LANG, QP.SCOPEQ, + ] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() val sub = getSubFromSecurityContext() - val includeSysAttrs = params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) - .contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) + val includeSysAttrs = queryParams.getOrDefault(QueryParameter.OPTIONS.key, emptyList()) + .contains(OptionsValue.SYS_ATTRS.value) val paginationQuery = parsePaginationParameters( - params, + queryParams, applicationProperties.pagination.limitDefault, applicationProperties.pagination.limitMax ).bind() @@ -107,7 +120,7 @@ class ContextSourceRegistrationHandler( contextSourceRegistrationsCount, "/ngsi-ld/v1/csourceRegistrations", paginationQuery, - params, + queryParams, mediaType, contexts ) @@ -123,9 +136,11 @@ class ContextSourceRegistrationHandler( suspend fun getByURI( @RequestHeader httpHeaders: HttpHeaders, @PathVariable contextSourceRegistrationId: URI, - @RequestParam options: String? + @AllowedParameters(implemented = [QP.OPTIONS]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { - val includeSysAttrs = options == QUERY_PARAM_OPTIONS_SYSATTRS_VALUE + val options = queryParams.getFirst(QP.OPTIONS.key) + val includeSysAttrs = options?.contains(OptionsValue.SYS_ATTRS.value) ?: false val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() @@ -144,7 +159,11 @@ class ContextSourceRegistrationHandler( * Implements 6.9.3.3 - Delete ContextSourceRegistration */ @DeleteMapping("/{contextSourceRegistrationId}") - suspend fun delete(@PathVariable contextSourceRegistrationId: URI): ResponseEntity<*> = either { + suspend fun delete( + @PathVariable contextSourceRegistrationId: URI, + @AllowedParameters // no query parameter is defined in the specification + @RequestParam queryParams: MultiValueMap + ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() checkIsAllowed(contextSourceRegistrationId, sub).bind() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt index b4d1a37fd..019407bc3 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/AttributeHandler.kt @@ -3,6 +3,8 @@ package com.egm.stellio.search.discovery.web import arrow.core.raise.either import com.egm.stellio.search.discovery.service.AttributeService import com.egm.stellio.shared.config.ApplicationProperties +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.QP import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm import com.egm.stellio.shared.util.JsonUtils @@ -13,16 +15,18 @@ import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity +import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import java.util.Optional @RestController @RequestMapping("/ngsi-ld/v1/attributes") +@Validated class AttributeHandler( private val attributeService: AttributeService, private val applicationProperties: ApplicationProperties @@ -33,11 +37,14 @@ class AttributeHandler( @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getAttributes( @RequestHeader httpHeaders: HttpHeaders, - @RequestParam details: Optional + @AllowedParameters(implemented = [QP.DETAILS], notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { + val details = queryParams.getFirst(QP.DETAILS.key)?.toBoolean() + val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() - val detailedRepresentation = details.orElse(false) + val detailedRepresentation = details ?: false val availableAttribute: Any = if (detailedRepresentation) attributeService.getAttributeDetails(contexts) @@ -56,7 +63,9 @@ class AttributeHandler( @GetMapping("/{attrId}", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getByAttributeId( @RequestHeader httpHeaders: HttpHeaders, - @PathVariable attrId: String + @PathVariable attrId: String, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt index 1cf4ebb9c..2a86fcb2a 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/discovery/web/EntityTypeHandler.kt @@ -3,6 +3,8 @@ package com.egm.stellio.search.discovery.web import arrow.core.raise.either import com.egm.stellio.search.discovery.service.EntityTypeService import com.egm.stellio.shared.config.ApplicationProperties +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.QP import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm import com.egm.stellio.shared.util.JsonUtils @@ -13,16 +15,18 @@ import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity +import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import java.util.Optional @RestController @RequestMapping("/ngsi-ld/v1/types") +@Validated class EntityTypeHandler( private val entityTypeService: EntityTypeService, private val applicationProperties: ApplicationProperties @@ -34,11 +38,14 @@ class EntityTypeHandler( @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getTypes( @RequestHeader httpHeaders: HttpHeaders, - @RequestParam details: Optional + @AllowedParameters(implemented = [QP.DETAILS], notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { + val details = queryParams.getFirst(QP.DETAILS.key)?.toBoolean() + val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() - val detailedRepresentation = details.orElse(false) + val detailedRepresentation = details ?: false val availableEntityTypes: Any = if (detailedRepresentation) entityTypeService.getEntityTypes(contexts) @@ -58,7 +65,9 @@ class EntityTypeHandler( @GetMapping("/{type}", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getByType( @RequestHeader httpHeaders: HttpHeaders, - @PathVariable type: String + @PathVariable type: String, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/EntitiesQuery.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/EntitiesQuery.kt index ea7569424..61ecb6a54 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/EntitiesQuery.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/model/EntitiesQuery.kt @@ -3,9 +3,9 @@ package com.egm.stellio.search.entity.model import com.egm.stellio.shared.model.EntitySelector import com.egm.stellio.shared.model.EntityTypeSelection import com.egm.stellio.shared.model.ExpandedTerm -import com.egm.stellio.shared.model.GeoQuery -import com.egm.stellio.shared.model.LinkedEntityQuery -import com.egm.stellio.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.GeoQuery +import com.egm.stellio.shared.queryparameter.LinkedEntityQuery +import com.egm.stellio.shared.queryparameter.PaginationQuery import java.net.URI sealed class EntitiesQuery( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt index 23e2a7ec1..7c756b569 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/EntityQueryService.kt @@ -22,7 +22,6 @@ import com.egm.stellio.shared.model.AlreadyExistsException import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.util.Sub -import com.egm.stellio.shared.util.buildGeoQuery import com.egm.stellio.shared.util.buildQQuery import com.egm.stellio.shared.util.buildScopeQQuery import com.egm.stellio.shared.util.buildTypeQuery @@ -131,7 +130,7 @@ class EntityQueryService( } ?: sqlFilter }.let { sqlFilter -> entitiesQuery.geoQuery?.let { geoQuery -> - sqlFilter.wrapToAndClause(buildGeoQuery(geoQuery)) + sqlFilter.wrapToAndClause(geoQuery.buildSqlFilter()) } ?: sqlFilter } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/LinkedEntityService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/LinkedEntityService.kt index 79a23eca4..82f328dd6 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/LinkedEntityService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/service/LinkedEntityService.kt @@ -7,11 +7,11 @@ import com.egm.stellio.search.entity.model.EntitiesQuery import com.egm.stellio.search.entity.model.EntitiesQueryFromGet import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.CompactedEntity -import com.egm.stellio.shared.model.LinkedEntityQuery -import com.egm.stellio.shared.model.LinkedEntityQuery.JoinType -import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.model.getRelationshipsObjects import com.egm.stellio.shared.model.inlineLinkedEntities +import com.egm.stellio.shared.queryparameter.LinkedEntityQuery +import com.egm.stellio.shared.queryparameter.LinkedEntityQuery.Companion.JoinType +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID_TERM import com.egm.stellio.shared.util.JsonLdUtils.compactEntities diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt index e64425f41..0fcba5de6 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/entity/util/EntitiesQueryUtils.kt @@ -10,56 +10,47 @@ 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.EntitySelector +import com.egm.stellio.shared.queryparameter.GeoQuery.Companion.parseGeoQueryParameters +import com.egm.stellio.shared.queryparameter.LinkedEntityQuery.Companion.parseLinkedEntityQueryParameters +import com.egm.stellio.shared.queryparameter.PaginationQuery.Companion.parsePaginationParameters +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.JsonLdUtils -import com.egm.stellio.shared.util.QUERY_PARAM_ATTRS -import com.egm.stellio.shared.util.QUERY_PARAM_CONTAINED_BY -import com.egm.stellio.shared.util.QUERY_PARAM_DATASET_ID -import com.egm.stellio.shared.util.QUERY_PARAM_ID -import com.egm.stellio.shared.util.QUERY_PARAM_ID_PATTERN -import com.egm.stellio.shared.util.QUERY_PARAM_JOIN -import com.egm.stellio.shared.util.QUERY_PARAM_JOIN_LEVEL -import com.egm.stellio.shared.util.QUERY_PARAM_Q -import com.egm.stellio.shared.util.QUERY_PARAM_SCOPEQ -import com.egm.stellio.shared.util.QUERY_PARAM_TYPE import com.egm.stellio.shared.util.decode import com.egm.stellio.shared.util.expandTypeSelection -import com.egm.stellio.shared.util.parseAndExpandRequestParameter -import com.egm.stellio.shared.util.parseGeoQueryParameters -import com.egm.stellio.shared.util.parseLinkedEntityQueryParameters -import com.egm.stellio.shared.util.parsePaginationParameters -import com.egm.stellio.shared.util.parseRequestParameter +import com.egm.stellio.shared.util.parseAndExpandQueryParameter +import com.egm.stellio.shared.util.parseQueryParameter import com.egm.stellio.shared.util.toListOfUri import com.egm.stellio.shared.util.validateIdPattern import org.springframework.util.MultiValueMap fun composeEntitiesQueryFromGet( defaultPagination: ApplicationProperties.Pagination, - requestParams: MultiValueMap, + queryParams: MultiValueMap, contexts: List ): Either = either { - val ids = requestParams.getFirst(QUERY_PARAM_ID)?.split(",").orEmpty().toListOfUri().toSet() - val typeSelection = expandTypeSelection(requestParams.getFirst(QUERY_PARAM_TYPE), contexts) - val idPattern = validateIdPattern(requestParams.getFirst(QUERY_PARAM_ID_PATTERN)).bind() + val ids = queryParams.getFirst(QueryParameter.ID.key)?.split(",").orEmpty().toListOfUri().toSet() + val typeSelection = expandTypeSelection(queryParams.getFirst(QueryParameter.TYPE.key), contexts) + val idPattern = validateIdPattern(queryParams.getFirst(QueryParameter.ID_PATTERN.key)).bind() /** * Decoding query parameters is not supported by default so a call to a decode function was added query * with the right parameters values */ - val q = requestParams.getFirst(QUERY_PARAM_Q)?.decode() - val scopeQ = requestParams.getFirst(QUERY_PARAM_SCOPEQ) - val attrs = parseAndExpandRequestParameter(requestParams.getFirst(QUERY_PARAM_ATTRS), contexts) - val datasetId = parseRequestParameter(requestParams.getFirst(QUERY_PARAM_DATASET_ID)) + val q = queryParams.getFirst(QueryParameter.Q.key)?.decode() + val scopeQ = queryParams.getFirst(QueryParameter.SCOPEQ.key) + val attrs = parseAndExpandQueryParameter(queryParams.getFirst(QueryParameter.ATTRS.key), contexts) + val datasetId = parseQueryParameter(queryParams.getFirst(QueryParameter.DATASET_ID.key)) val paginationQuery = parsePaginationParameters( - requestParams, + queryParams, defaultPagination.limitDefault, defaultPagination.limitMax ).bind() - val geoQuery = parseGeoQueryParameters(requestParams.toSingleValueMap(), contexts).bind() + val geoQuery = parseGeoQueryParameters(queryParams.toSingleValueMap(), contexts).bind() val linkedEntityQuery = parseLinkedEntityQueryParameters( - requestParams.getFirst(QUERY_PARAM_JOIN), - requestParams.getFirst(QUERY_PARAM_JOIN_LEVEL), - requestParams.getFirst(QUERY_PARAM_CONTAINED_BY) + queryParams.getFirst(QueryParameter.JOIN.key), + queryParams.getFirst(QueryParameter.JOIN_LEVEL.key), + queryParams.getFirst(QueryParameter.CONTAINED_BY.key) ).bind() EntitiesQueryFromGet( @@ -94,7 +85,7 @@ fun EntitiesQueryFromGet.validateMinimalQueryEntitiesParameters(): Either, + queryParams: MultiValueMap, contexts: List ): Either = either { val entitySelectors = query.entities?.map { entitySelector -> @@ -123,7 +114,7 @@ fun composeEntitiesQueryFromPost( ).bind() val paginationQuery = parsePaginationParameters( - requestParams, + queryParams, defaultPagination.limitDefault, defaultPagination.limitMax ).bind() 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 eda26b8a9..acedd6a9a 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 @@ -20,10 +20,15 @@ import com.egm.stellio.search.entity.util.validateMinimalQueryEntitiesParameters import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.ExpandedEntity +import com.egm.stellio.shared.model.NgsiLdDataRepresentation.Companion.parseRepresentations import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.filterAttributes import com.egm.stellio.shared.model.toFinalRepresentation import com.egm.stellio.shared.model.toNgsiLdEntity +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.OptionsValue +import com.egm.stellio.shared.queryparameter.QP +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.GEO_JSON_CONTENT_TYPE import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JSON_MERGE_PATCH_CONTENT_TYPE @@ -34,15 +39,12 @@ import com.egm.stellio.shared.util.JsonLdUtils.expandAttributes import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm import com.egm.stellio.shared.util.JsonUtils.serializeObject -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS_OBSERVEDAT_VALUE import com.egm.stellio.shared.util.buildQueryResponse import com.egm.stellio.shared.util.extractPayloadAndContexts import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext import com.egm.stellio.shared.util.missingPathErrorResponse -import com.egm.stellio.shared.util.parseRepresentations import com.egm.stellio.shared.util.parseTimeParameter import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders import com.egm.stellio.shared.util.toUri @@ -52,6 +54,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType.APPLICATION_JSON_VALUE import org.springframework.http.ResponseEntity import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping @@ -65,10 +68,10 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.net.URI -import java.util.Optional @RestController @RequestMapping("/ngsi-ld/v1/entities") +@Validated class EntityHandler( private val applicationProperties: ApplicationProperties, private val entityService: EntityService, @@ -83,7 +86,9 @@ class EntityHandler( @PostMapping(consumes = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun create( @RequestHeader httpHeaders: HttpHeaders, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(implemented = [], notImplemented = [QueryParameter.LOCAL, QueryParameter.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = @@ -108,13 +113,17 @@ class EntityHandler( suspend fun merge( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestParam options: MultiValueMap, + @AllowedParameters( + implemented = [QP.OBSERVED_AT], + notImplemented = [QP.FORMAT, QP.OPTIONS, QP.TYPE, QP.LANG, QP.LOCAL, QP.VIA] + ) + @RequestParam queryParams: MultiValueMap, @RequestBody requestBody: Mono ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() - val observedAt = options.getFirst(QUERY_PARAM_OPTIONS_OBSERVEDAT_VALUE) + val observedAt = queryParams.getFirst(QueryParameter.OBSERVED_AT.key) ?.parseTimeParameter("'observedAt' parameter is not a valid date") ?.getOrElse { return@either BadRequestDataException(it).left().bind>() } val expandedAttributes = expandAttributes(body, contexts) @@ -146,7 +155,9 @@ class EntityHandler( suspend fun replace( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(implemented = [], notImplemented = [QueryParameter.LOCAL, QueryParameter.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = @@ -181,7 +192,15 @@ class EntityHandler( @GetMapping(produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, - @RequestParam params: MultiValueMap + @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 ): ResponseEntity<*> = either { val mediaType = getApplicableMediaType(httpHeaders).bind() val sub = getSubFromSecurityContext() @@ -189,7 +208,7 @@ class EntityHandler( val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val entitiesQuery = composeEntitiesQueryFromGet( applicationProperties.pagination, - params, + queryParams, contexts ).bind() .validateMinimalQueryEntitiesParameters().bind() @@ -203,13 +222,13 @@ class EntityHandler( linkedEntityService.processLinkedEntities(it, entitiesQuery, sub.getOrNull()).bind() } - val ngsiLdDataRepresentation = parseRepresentations(params, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) buildQueryResponse( compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation), count, "/ngsi-ld/v1/entities", entitiesQuery.paginationQuery, - params, + queryParams, mediaType, contexts ) @@ -225,7 +244,14 @@ class EntityHandler( suspend fun getByURI( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestParam params: MultiValueMap + @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 ): ResponseEntity<*> = either { val mediaType = getApplicableMediaType(httpHeaders).bind() val sub = getSubFromSecurityContext() @@ -233,7 +259,7 @@ class EntityHandler( val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val entitiesQuery = composeEntitiesQueryFromGet( applicationProperties.pagination, - params, + queryParams, contexts ).bind() @@ -258,7 +284,7 @@ class EntityHandler( httpHeaders, csr, "/ngsi-ld/v1/entities/$entityId", - params + queryParams ) contextSourceRegistrationService.updateContextSourceStatus(csr, response.isRight()) response.map { it?.let { it to csr } } @@ -283,7 +309,7 @@ class EntityHandler( val mergedEntityWithLinkedEntities = linkedEntityService.processLinkedEntities(mergedEntity, entitiesQuery, sub.getOrNull()).bind() - val ngsiLdDataRepresentation = parseRepresentations(params, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) prepareGetSuccessResponseHeaders(mediaType, contexts) .let { val body = if (mergedEntityWithLinkedEntities.size == 1) @@ -303,7 +329,9 @@ class EntityHandler( */ @DeleteMapping("/{entityId}") suspend fun delete( - @PathVariable entityId: URI + @PathVariable entityId: URI, + @AllowedParameters(implemented = [], notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -323,11 +351,16 @@ class EntityHandler( suspend fun appendEntityAttributes( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestParam options: Optional, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters( + implemented = [QP.OPTIONS], + notImplemented = [QP.TYPE, QP.LOCAL, QP.VIA] // type is for dist-ops + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { + val options = queryParams.getFirst(QueryParameter.OPTIONS.key) val sub = getSubFromSecurityContext() - val disallowOverwrite = options.map { it == QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE }.orElse(false) + val disallowOverwrite = options?.let { it == OptionsValue.NO_OVERWRITE.value } ?: false val (body, contexts) = extractPayloadAndContexts(requestBody, httpHeaders, applicationProperties.contexts.core).bind() @@ -364,7 +397,9 @@ class EntityHandler( suspend fun updateEntityAttributes( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(implemented = [], notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = @@ -402,7 +437,9 @@ class EntityHandler( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(implemented = [], notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -441,11 +478,15 @@ class EntityHandler( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, - @RequestParam params: MultiValueMap + @AllowedParameters( + implemented = [QP.DELETE_ALL, QP.DATASET_ID], + notImplemented = [QP.LOCAL, QP.TYPE, QP.VIA] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - val deleteAll = params.getFirst("deleteAll")?.toBoolean() ?: false - val datasetId = params.getFirst("datasetId")?.toUri() + val deleteAll = queryParams.getFirst(QueryParameter.DELETE_ALL.key)?.toBoolean() ?: false + val datasetId = queryParams.getFirst(QueryParameter.DATASET_ID.key)?.toUri() val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val expandedAttrId = expandJsonLdTerm(attrId, contexts) @@ -476,7 +517,9 @@ class EntityHandler( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(implemented = [], notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = 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 1fa5f2d3f..61f59326c 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 @@ -13,9 +13,13 @@ 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.LdContextNotAvailableException +import com.egm.stellio.shared.model.NgsiLdDataRepresentation.Companion.parseRepresentations import com.egm.stellio.shared.model.filterAttributes import com.egm.stellio.shared.model.toFinalRepresentation import com.egm.stellio.shared.model.toNgsiLdEntity +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.OptionsValue +import com.egm.stellio.shared.queryparameter.QP import com.egm.stellio.shared.util.GEO_JSON_CONTENT_TYPE import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE @@ -24,7 +28,6 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID_TERM import com.egm.stellio.shared.util.JsonLdUtils.compactEntities import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntityF import com.egm.stellio.shared.util.JsonUtils.deserializeAsList -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE import com.egm.stellio.shared.util.addCoreContextIfMissing import com.egm.stellio.shared.util.buildQueryResponse import com.egm.stellio.shared.util.checkContentIsNgsiLdSupported @@ -35,7 +38,6 @@ import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeader import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext -import com.egm.stellio.shared.util.parseRepresentations import com.egm.stellio.shared.util.toListOfUri import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders @@ -44,6 +46,7 @@ import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.util.LinkedMultiValueMap import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader @@ -51,10 +54,10 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono -import java.util.Optional @RestController @RequestMapping("/ngsi-ld/v1/entityOperations") +@Validated class EntityOperationHandler( private val applicationProperties: ApplicationProperties, private val entityOperationService: EntityOperationService, @@ -67,7 +70,9 @@ class EntityOperationHandler( @PostMapping("/create", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun create( @RequestHeader httpHeaders: HttpHeaders, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(implemented = [], notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -98,8 +103,13 @@ class EntityOperationHandler( suspend fun upsert( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono, - @RequestParam(required = false) options: String? + @AllowedParameters( + implemented = [QP.OPTIONS], + notImplemented = [QP.LOCAL, QP.VIA] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { + val options = queryParams.getFirst(QP.OPTIONS.key) val sub = getSubFromSecurityContext() val (parsedEntities, unparsableEntities) = prepareEntitiesFromRequestBody(requestBody, httpHeaders).bind() @@ -138,13 +148,18 @@ class EntityOperationHandler( suspend fun update( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono, - @RequestParam options: Optional + @AllowedParameters( + implemented = [QP.OPTIONS], + notImplemented = [QP.LOCAL, QP.VIA] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { + val options = queryParams.getFirst(QP.OPTIONS.key) val sub = getSubFromSecurityContext() val (parsedEntities, unparsableEntities) = prepareEntitiesFromRequestBody(requestBody, httpHeaders).bind() - val disallowOverwrite = options.map { it == QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE }.orElse(false) + val disallowOverwrite = options?.let { it == OptionsValue.NO_OVERWRITE.value } ?: false val batchOperationResult = BatchOperationResult().apply { addEntitiesToErrors(unparsableEntities) @@ -174,6 +189,8 @@ class EntityOperationHandler( suspend fun merge( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -203,7 +220,11 @@ class EntityOperationHandler( * Implements 6.17.3.1 - Delete Batch of Entities */ @PostMapping("/delete", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) - suspend fun delete(@RequestBody requestBody: Mono>): ResponseEntity<*> = either { + suspend fun delete( + @RequestBody requestBody: Mono>, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap + ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val body = requestBody.awaitFirst() @@ -234,7 +255,11 @@ class EntityOperationHandler( suspend fun queryEntitiesViaPost( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono, - @RequestParam params: MultiValueMap + @AllowedParameters( + implemented = [QP.LIMIT, QP.OFFSET, QP.COUNT, QP.OPTIONS], + notImplemented = [QP.LOCAL, QP.VIA] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() @@ -244,7 +269,7 @@ class EntityOperationHandler( val entitiesQuery = composeEntitiesQueryFromPost( applicationProperties.pagination, query, - params, + queryParams, contexts ).bind() @@ -254,7 +279,7 @@ class EntityOperationHandler( val compactedEntities = compactEntities(filteredEntities, contexts) - val ngsiLdDataRepresentation = parseRepresentations(params, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) .copy(languageFilter = query.lang) buildQueryResponse( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryParamsUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryParamsUtils.kt index 26f2e461e..8ed9248d8 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryParamsUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/util/TemporalQueryParamsUtils.kt @@ -1,10 +1 @@ package com.egm.stellio.search.temporal.util - -const val TIMEREL_PARAM = "timerel" -const val TIMEAT_PARAM = "timeAt" -const val ENDTIMEAT_PARAM = "endTimeAt" -const val AGGRPERIODDURATION_PARAM = "aggrPeriodDuration" -const val AGGRMETHODS_PARAM = "aggrMethods" -const val LASTN_PARAM = "lastN" -const val TIMEPROPERTY_PARAM = "timeproperty" -const val WHOLE_TIME_RANGE_DURATION = "PT0S" 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 e1cc82414..494a8a5ee 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,8 @@ 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.queryparameter.QueryParameter import com.egm.stellio.shared.util.OptionsParamValue -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS import com.egm.stellio.shared.util.hasValueInOptionsParam import com.egm.stellio.shared.util.parseTimeParameter import org.springframework.util.MultiValueMap @@ -29,6 +29,8 @@ import org.springframework.util.MultiValueMapAdapter import java.time.ZonedDateTime import java.util.Optional +const val WHOLE_TIME_RANGE_DURATION = "PT0S" + fun composeTemporalEntitiesQueryFromGet( defaultPagination: ApplicationProperties.Pagination, requestParams: MultiValueMap, @@ -45,15 +47,15 @@ fun composeTemporalEntitiesQueryFromGet( entitiesQueryFromGet.validateMinimalQueryEntitiesParameters().bind() val withTemporalValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), OptionsParamValue.TEMPORAL_VALUES ) val withAudit = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), OptionsParamValue.AUDIT ) val withAggregatedValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), OptionsParamValue.AGGREGATED_VALUES ) val temporalQuery = @@ -82,26 +84,26 @@ fun composeTemporalEntitiesQueryFromPost( ).bind() val withTemporalValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), OptionsParamValue.TEMPORAL_VALUES ) val withAudit = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), OptionsParamValue.AUDIT ) val withAggregatedValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key)), OptionsParamValue.AGGREGATED_VALUES ) val temporalParams = mapOf( - TIMEREL_PARAM to listOf(query.temporalQ?.timerel), - TIMEAT_PARAM to listOf(query.temporalQ?.timeAt), - ENDTIMEAT_PARAM to listOf(query.temporalQ?.endTimeAt), - AGGRPERIODDURATION_PARAM to listOf(query.temporalQ?.aggrPeriodDuration), - AGGRMETHODS_PARAM to query.temporalQ?.aggrMethods, - LASTN_PARAM to listOf(query.temporalQ?.lastN.toString()), - TIMEPROPERTY_PARAM to listOf(query.temporalQ?.timeproperty) + QueryParameter.TIMEREL.key to listOf(query.temporalQ?.timerel), + QueryParameter.TIMEAT.key to listOf(query.temporalQ?.timeAt), + QueryParameter.ENDTIMEAT.key to listOf(query.temporalQ?.endTimeAt), + QueryParameter.AGGRPERIODDURATION.key to listOf(query.temporalQ?.aggrPeriodDuration), + QueryParameter.AGGRMETHODS.key to query.temporalQ?.aggrMethods, + QueryParameter.LASTN.key to listOf(query.temporalQ?.lastN.toString()), + QueryParameter.TIMEPROPERTY.key to listOf(query.temporalQ?.timeproperty) ) val temporalQuery = buildTemporalQuery( MultiValueMapAdapter(temporalParams), @@ -126,16 +128,16 @@ fun buildTemporalQuery( inQueryEntities: Boolean = false, withAggregatedValues: Boolean = false, ): Either { - val timerelParam = params.getFirst(TIMEREL_PARAM) - val timeAtParam = params.getFirst(TIMEAT_PARAM) - val endTimeAtParam = params.getFirst(ENDTIMEAT_PARAM) + val timerelParam = params.getFirst(QueryParameter.TIMEREL.key) + val timeAtParam = params.getFirst(QueryParameter.TIMEAT.key) + val endTimeAtParam = params.getFirst(QueryParameter.ENDTIMEAT.key) val aggrPeriodDurationParam = if (withAggregatedValues) - params.getFirst(AGGRPERIODDURATION_PARAM) ?: WHOLE_TIME_RANGE_DURATION + params.getFirst(QueryParameter.AGGRPERIODDURATION.key) ?: WHOLE_TIME_RANGE_DURATION else null - val aggrMethodsParam = params.getFirst(AGGRMETHODS_PARAM) - val lastNParam = params.getFirst(LASTN_PARAM) - val timeproperty = params.getFirst(TIMEPROPERTY_PARAM)?.let { + val aggrMethodsParam = params.getFirst(QueryParameter.AGGRMETHODS.key) + val lastNParam = params.getFirst(QueryParameter.LASTN.key) + val timeproperty = params.getFirst(QueryParameter.TIMEPROPERTY.key)?.let { AttributeInstance.TemporalProperty.forPropertyName(it) } ?: AttributeInstance.TemporalProperty.OBSERVED_AT 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 4fc12e749..3804985e8 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 @@ -3,10 +3,10 @@ package com.egm.stellio.search.temporal.web import com.egm.stellio.search.temporal.model.TemporalEntitiesQuery import com.egm.stellio.search.temporal.model.TemporalQuery import com.egm.stellio.shared.model.CompactedEntity +import com.egm.stellio.shared.model.NgsiLdDataRepresentation.Companion.parseRepresentations import com.egm.stellio.shared.model.toFinalRepresentation import com.egm.stellio.shared.util.JsonUtils.serializeObject import com.egm.stellio.shared.util.buildQueryResponse -import com.egm.stellio.shared.util.parseRepresentations import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders import com.egm.stellio.shared.util.toHttpHeaderFormat import org.springframework.http.HttpHeaders @@ -32,8 +32,11 @@ object TemporalApiResponses { lang: String? = null, ): ResponseEntity { val baseRepresentation = parseRepresentations(requestParams, mediaType) - - val representation = lang?.let { baseRepresentation.copy(languageFilter = it) } ?: baseRepresentation + // 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) + } + ?: baseRepresentation.copy(timeproperty = query.temporalQuery.timeproperty.propertyName) val successResponse = buildQueryResponse( entities.toFinalRepresentation(representation), 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 b2d084260..158ff122f 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 @@ -14,9 +14,12 @@ 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.ExpandedAttributes +import com.egm.stellio.shared.model.NgsiLdDataRepresentation.Companion.parseRepresentations import com.egm.stellio.shared.model.getMemberValueAsDateTime import com.egm.stellio.shared.model.toExpandedAttributes import com.egm.stellio.shared.model.toFinalRepresentation +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.QP import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JSON_MERGE_PATCH_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY @@ -34,7 +37,6 @@ import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext import com.egm.stellio.shared.util.invalidTemporalInstanceMessage import com.egm.stellio.shared.util.missingPathErrorResponse -import com.egm.stellio.shared.util.parseRepresentations import com.egm.stellio.shared.util.toUri import com.egm.stellio.shared.web.BaseHandler import org.springframework.http.HttpHeaders @@ -42,6 +44,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping @@ -57,6 +60,7 @@ import java.net.URI @RestController @RequestMapping("/ngsi-ld/v1/temporal/entities") +@Validated class TemporalEntityHandler( private val temporalService: TemporalService, private val temporalQueryService: TemporalQueryService, @@ -70,7 +74,9 @@ class TemporalEntityHandler( @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun create( @RequestHeader httpHeaders: HttpHeaders, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = @@ -106,7 +112,9 @@ class TemporalEntityHandler( suspend fun addAttributes( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -133,14 +141,22 @@ class TemporalEntityHandler( @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getForEntities( @RequestHeader httpHeaders: HttpHeaders, - @RequestParam params: MultiValueMap + @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.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] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() val temporalEntitiesQuery = - composeTemporalEntitiesQueryFromGet(applicationProperties.pagination, params, contexts, true).bind() + composeTemporalEntitiesQueryFromGet(applicationProperties.pagination, queryParams, contexts, true).bind() val (temporalEntities, total, range) = temporalQueryService.queryTemporalEntities( temporalEntitiesQuery, @@ -154,7 +170,7 @@ class TemporalEntityHandler( total, "/ngsi-ld/v1/temporal/entities", temporalEntitiesQuery, - params, + queryParams, mediaType, contexts, range @@ -171,14 +187,21 @@ class TemporalEntityHandler( suspend fun getForEntity( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, - @RequestParam params: MultiValueMap + @AllowedParameters( + implemented = [ + QP.OPTIONS, 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] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() val temporalEntitiesQuery = - composeTemporalEntitiesQueryFromGet(applicationProperties.pagination, params, contexts).bind() + composeTemporalEntitiesQueryFromGet(applicationProperties.pagination, queryParams, contexts).bind() val (temporalEntity, range) = temporalQueryService.queryTemporalEntity( entityId, @@ -188,7 +211,7 @@ class TemporalEntityHandler( val compactedEntity = compactEntity(temporalEntity, contexts) - val ngsiLdDataRepresentation = parseRepresentations(params, mediaType) + val ngsiLdDataRepresentation = parseRepresentations(queryParams, mediaType) buildEntityTemporalResponse(mediaType, contexts, temporalEntitiesQuery, range) .body(serializeObject(compactedEntity.toFinalRepresentation(ngsiLdDataRepresentation))) }.fold( @@ -209,7 +232,9 @@ class TemporalEntityHandler( @PathVariable entityId: URI, @PathVariable attrId: String, @PathVariable instanceId: URI, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val (body, contexts) = @@ -246,7 +271,9 @@ class TemporalEntityHandler( */ @DeleteMapping("/{entityId}") suspend fun deleteTemporalEntity( - @PathVariable entityId: URI + @PathVariable entityId: URI, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() @@ -266,11 +293,15 @@ class TemporalEntityHandler( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, - @RequestParam params: MultiValueMap + @AllowedParameters( + implemented = [QP.DELETE_ALL, QP.DATASET_ID], + notImplemented = [QP.LOCAL, QP.VIA] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() - val deleteAll = params.getFirst("deleteAll")?.toBoolean() ?: false - val datasetId = params.getFirst("datasetId")?.toUri() + val deleteAll = queryParams.getFirst(QP.DELETE_ALL.key)?.toBoolean() ?: false + val datasetId = queryParams.getFirst(QP.DATASET_ID.key)?.toUri() val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() attrId.checkNameIsNgsiLdSupported().bind() @@ -302,7 +333,9 @@ class TemporalEntityHandler( @RequestHeader httpHeaders: HttpHeaders, @PathVariable entityId: URI, @PathVariable attrId: String, - @PathVariable instanceId: URI + @PathVariable instanceId: URI, + @AllowedParameters(notImplemented = [QP.LOCAL, QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt index 0a7065922..7ad929866 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/temporal/web/TemporalEntityOperationsHandler.kt @@ -6,6 +6,8 @@ import com.egm.stellio.search.temporal.service.TemporalQueryService import com.egm.stellio.search.temporal.util.composeTemporalEntitiesQueryFromPost import com.egm.stellio.search.temporal.web.TemporalApiResponses.buildEntitiesTemporalResponse import com.egm.stellio.shared.config.ApplicationProperties +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.QP import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils.compactEntities import com.egm.stellio.shared.util.getApplicableMediaType @@ -16,6 +18,7 @@ import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader @@ -26,6 +29,7 @@ import reactor.core.publisher.Mono @RestController @RequestMapping("/ngsi-ld/v1/temporal/entityOperations") +@Validated class TemporalEntityOperationsHandler( private val temporalQueryService: TemporalQueryService, private val applicationProperties: ApplicationProperties @@ -38,7 +42,13 @@ class TemporalEntityOperationsHandler( suspend fun queryEntitiesViaPost( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono, - @RequestParam params: MultiValueMap + @AllowedParameters( + implemented = [ + QP.LIMIT, QP.OFFSET, QP.COUNT, QP.OPTIONS + ], + notImplemented = [QP.LOCAL, QP.VIA] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val sub = getSubFromSecurityContext() val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() @@ -49,7 +59,7 @@ class TemporalEntityOperationsHandler( composeTemporalEntitiesQueryFromPost( applicationProperties.pagination, query, - params, + queryParams, contexts ).bind() @@ -65,7 +75,7 @@ class TemporalEntityOperationsHandler( total, "/ngsi-ld/v1/temporal/entities", temporalEntitiesQuery, - params, + queryParams, mediaType, contexts, range, diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/AuthorizationServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/AuthorizationServiceTests.kt index f434cdf06..0fd348842 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/AuthorizationServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/AuthorizationServiceTests.kt @@ -3,7 +3,7 @@ package com.egm.stellio.search.authorization.service import arrow.core.None import com.egm.stellio.search.entity.model.EntitiesQueryFromGet import com.egm.stellio.shared.config.ApplicationProperties -import com.egm.stellio.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.AUTHZ_TEST_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.shouldSucceedWith import com.egm.stellio.shared.util.toUri diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationServiceTests.kt index 859e64995..2d0cc8d69 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EnabledAuthorizationServiceTests.kt @@ -9,7 +9,7 @@ import com.egm.stellio.search.authorization.model.Group import com.egm.stellio.search.authorization.model.User import com.egm.stellio.search.entity.model.EntitiesQueryFromGet import com.egm.stellio.shared.model.AccessDeniedException -import com.egm.stellio.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.AUTHZ_TEST_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.AccessRight diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsServiceTests.kt index 2de75c800..6365035a4 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/service/EntityAccessRightsServiceTests.kt @@ -13,7 +13,7 @@ import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.search.support.buildSapAttribute import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.ExpandedTerm -import com.egm.stellio.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIARY_TYPE import com.egm.stellio.shared.util.AUTHZ_TEST_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.AccessRight diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/csr/service/ContextSourceCallerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/csr/service/ContextSourceCallerTests.kt index 39d30f201..31c8a4616 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/csr/service/ContextSourceCallerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/csr/service/ContextSourceCallerTests.kt @@ -4,11 +4,11 @@ import com.egm.stellio.search.csr.CsrUtils.gimmeRawCSR import com.egm.stellio.search.csr.model.MiscellaneousPersistentWarning import com.egm.stellio.search.csr.model.MiscellaneousWarning import com.egm.stellio.search.csr.model.RevalidationFailedWarning +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXT import com.egm.stellio.shared.util.GEO_JSON_CONTENT_TYPE import com.egm.stellio.shared.util.GEO_JSON_MEDIA_TYPE import com.egm.stellio.shared.util.JsonUtils.serializeObject -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS import com.egm.stellio.shared.util.assertJsonPayloadsAreEqual import com.github.tomakehurst.wiremock.client.WireMock.get import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor @@ -175,7 +175,7 @@ class ContextSourceCallerTests { get(urlMatching(path)) .willReturn(notFound()) ) - val params = LinkedMultiValueMap(mapOf(QUERY_PARAM_OPTIONS to listOf("simplified"))) + val params = LinkedMultiValueMap(mapOf(QueryParameter.OPTIONS.key to listOf("simplified"))) ContextSourceCaller.getDistributedInformation( HttpHeaders.EMPTY, csr, @@ -184,7 +184,7 @@ class ContextSourceCallerTests { ) verify( getRequestedFor(urlPathEqualTo(path)) - .withQueryParam(QUERY_PARAM_OPTIONS, notContaining("simplified")) + .withQueryParam(QueryParameter.OPTIONS.key, notContaining("simplified")) ) } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceQueriesTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceQueriesTests.kt index d8c72d167..486e31a5f 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceQueriesTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/EntityServiceQueriesTests.kt @@ -9,8 +9,8 @@ import com.egm.stellio.search.support.WithKafkaContainer import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.search.temporal.service.AttributeInstanceService import com.egm.stellio.shared.model.EntitySelector -import com.egm.stellio.shared.model.GeoQuery -import com.egm.stellio.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.GeoQuery +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIARY_TYPE import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.AuthContextModel diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/LinkedEntityServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/LinkedEntityServiceTests.kt index d20dcc68c..b94f2ba8b 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/LinkedEntityServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/entity/service/LinkedEntityServiceTests.kt @@ -4,9 +4,9 @@ import arrow.core.right import com.egm.stellio.search.entity.model.EntitiesQueryFromGet import com.egm.stellio.shared.model.CompactedEntity import com.egm.stellio.shared.model.ExpandedEntity -import com.egm.stellio.shared.model.LinkedEntityQuery -import com.egm.stellio.shared.model.LinkedEntityQuery.JoinType -import com.egm.stellio.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.LinkedEntityQuery +import com.egm.stellio.shared.queryparameter.LinkedEntityQuery.Companion.JoinType +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap import com.egm.stellio.shared.util.JsonUtils.serializeObject import com.egm.stellio.shared.util.LINKED_ENTITY_COMPACT_TYPE 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 a4795b38e..b4e467a5a 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 @@ -9,13 +9,13 @@ 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.EntitySelector -import com.egm.stellio.shared.model.GeoQuery -import com.egm.stellio.shared.model.LinkedEntityQuery.JoinType +import com.egm.stellio.shared.queryparameter.GeoQuery +import com.egm.stellio.shared.queryparameter.Georel +import com.egm.stellio.shared.queryparameter.LinkedEntityQuery.Companion.JoinType 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 import com.egm.stellio.shared.util.BEEKEEPER_TYPE -import com.egm.stellio.shared.util.GEO_QUERY_GEOREL_EQUALS import com.egm.stellio.shared.util.INCOMING_PROPERTY import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVATION_SPACE_PROPERTY @@ -239,7 +239,7 @@ class EntitiesQueryUtilsTests { assertEquals("temperature>32", it.q) assertEquals(GeoQuery.GeometryType.POINT, it.geoQuery?.geometry) assertEquals("[1.0, 1.0]", it.geoQuery?.coordinates) - assertEquals(GEO_QUERY_GEOREL_EQUALS, it.geoQuery?.georel) + assertEquals(Georel.EQUALS.key, it.geoQuery?.georel) assertEquals(NGSILD_OBSERVATION_SPACE_PROPERTY, it.geoQuery?.geoproperty) assertEquals("/Nantes", it.scopeQ) assertEquals(setOf("urn:ngsi-ld:Dataset:Test1", "urn:ngsi-ld:Dataset:Test2"), it.datasetId) 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 d936067de..90180f484 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 @@ -25,8 +25,8 @@ import com.egm.stellio.shared.model.DEFAULT_DETAIL import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.InternalErrorException import com.egm.stellio.shared.model.NgsiLdEntity -import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.model.ResourceNotFoundException +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXT import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.APIC_HEADER_LINK @@ -311,6 +311,46 @@ class EntityHandlerTests { ) } + @Test + fun `create entity should return a 400 if it contains an invalid query parameter`() { + val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") + + webClient.post() + .uri("/ngsi-ld/v1/entities?invalid=invalid") + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isBadRequest + .expectBody().json( + """ + { + "type": "https://uri.etsi.org/ngsi-ld/errors/InvalidRequest", + "title": "The ['invalid'] parameters are not allowed on this endpoint. This endpoint does not accept any query parameters. ", + "detail": "$DEFAULT_DETAIL" + } + """.trimIndent() + ) + } + + @Test + fun `create entity should return a 501 if it contains a not implemented query parameter`() { + val jsonLdFile = ClassPathResource("/ngsild/aquac/breedingService.jsonld") + + webClient.post() + .uri("/ngsi-ld/v1/entities?local=true") + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(501) + .expectBody().json( + """ + { + "type": "https://uri.etsi.org/ngsi-ld/errors/NotImplemented", + "title": "The ['local'] parameters have not been implemented yet. This endpoint does not accept any query parameters. ", + "detail": "$DEFAULT_DETAIL" + } + """.trimIndent() + ) + } + fun initializeRetrieveEntityMocks() { val compactedEntity = slot() 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 5a0300487..dca484c85 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,8 +12,8 @@ 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.shared.model.PaginationQuery import com.egm.stellio.shared.model.getScopes +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.loadSampleData 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 17748681a..24e0b5d71 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 @@ -8,9 +8,9 @@ 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.shared.model.ExpandedTerm -import com.egm.stellio.shared.model.PaginationQuery import com.egm.stellio.shared.model.addNonReifiedTemporalProperty import com.egm.stellio.shared.model.getSingleEntry +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.BEEHIVE_TYPE import com.egm.stellio.shared.util.JsonLdUtils 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 f9f87a411..2d7bf7c20 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,7 +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.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXTS import com.egm.stellio.shared.util.INCOMING_PROPERTY import com.egm.stellio.shared.util.OUTGOING_PROPERTY 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 9adc72bf8..e04ae3310 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,8 +20,8 @@ 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.shared.model.PaginationQuery import com.egm.stellio.shared.model.ResourceNotFoundException +import com.egm.stellio.shared.queryparameter.PaginationQuery 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 diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt index e6665d52b..6eb784a24 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/CompactedEntity.kt @@ -6,6 +6,7 @@ import com.egm.stellio.shared.model.AttributeCompactedType.LANGUAGEPROPERTY import com.egm.stellio.shared.model.AttributeCompactedType.PROPERTY import com.egm.stellio.shared.model.AttributeCompactedType.RELATIONSHIP import com.egm.stellio.shared.model.AttributeCompactedType.VOCABPROPERTY +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.FEATURES_PROPERTY_TERM import com.egm.stellio.shared.util.FEATURE_COLLECTION_TYPE import com.egm.stellio.shared.util.FEATURE_TYPE @@ -36,7 +37,6 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SYSATTRS_TERMS import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_UNIT_CODE_TERM import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_TERM import com.egm.stellio.shared.util.PROPERTIES_PROPERTY_TERM -import com.egm.stellio.shared.util.QUERY_PARAM_LANG import com.egm.stellio.shared.util.toUri import java.net.URI import java.util.Locale @@ -136,7 +136,7 @@ private fun simplifyAttribute(value: Map): Any { } fun CompactedEntity.toFilteredLanguageProperties(languageFilter: String): CompactedEntity { - val transformationParameters = mapOf(QUERY_PARAM_LANG to languageFilter) + val transformationParameters = mapOf(QueryParameter.LANG.key to languageFilter) return this.mapValues { entry -> applyAttributeTransformation( entry, @@ -155,7 +155,7 @@ private fun filterMultiInstanceLanguageProperty( } private fun filterLanguageProperty(value: Map, transformationParameters: Map?): Any { - val languageFilter = transformationParameters?.get(QUERY_PARAM_LANG)!! + val languageFilter = transformationParameters?.get(QueryParameter.LANG.key)!! val attributeCompactedType = value[JSONLD_TYPE_TERM]?.let { AttributeCompactedType.forKey(value[JSONLD_TYPE_TERM] as String) } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/GeoQuery.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/GeoQuery.kt deleted file mode 100644 index c7ab43841..000000000 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/GeoQuery.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.egm.stellio.shared.model - -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY - -data class GeoQuery( - val georel: String, - val geometry: GeometryType, - val coordinates: String, - val wktCoordinates: WKTCoordinates, - var geoproperty: ExpandedTerm = NGSILD_LOCATION_PROPERTY -) { - enum class GeometryType(val type: String) { - POINT("Point"), - MULTIPOINT("MultiPoint"), - LINESTRING("LineString"), - MULTILINESTRING("MultiLineString"), - POLYGON("Polygon"), - MULTIPOLYGON("MultiPolygon"); - - companion object { - fun isSupportedType(type: String): Boolean = - entries.any { it.type == type } - - fun forType(type: String): GeometryType? = - entries.find { it.type == type } - } - } -} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/LinkedEntityQuery.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/LinkedEntityQuery.kt deleted file mode 100644 index f6bf3fef3..000000000 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/LinkedEntityQuery.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.egm.stellio.shared.model - -import java.net.URI -import kotlin.UInt - -data class LinkedEntityQuery( - val join: JoinType = JoinType.NONE, - val joinLevel: UInt = DEFAULT_JOIN_LEVEL.toUInt(), - val containedBy: Set = emptySet() -) { - companion object { - const val DEFAULT_JOIN_LEVEL = 1 - } - - enum class JoinType(val type: String) { - FLAT("flat"), - INLINE("inline"), - NONE("@none"); - - companion object { - fun forType(type: String): JoinType? = - entries.find { it.type == type } - } - } -} 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 f1983d7b4..ba2dbcfca 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,8 +1,12 @@ package com.egm.stellio.shared.model +import com.egm.stellio.shared.queryparameter.OptionsValue +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.GEO_JSON_MEDIA_TYPE import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_TERM import org.springframework.http.MediaType +import org.springframework.util.MultiValueMap /** * Wrapper data class used to convey possible NGSI-LD Data Representations for entities as defined in 4.5 @@ -17,7 +21,35 @@ data class NgsiLdDataRepresentation( val geometryProperty: String? = null, // In the case of a temporal property, do not remove this property if sysAttrs is not asked val timeproperty: String? = null -) +) { + companion object { + fun parseRepresentations( + queryParams: MultiValueMap, + acceptMediaType: MediaType + ): NgsiLdDataRepresentation { + val optionsParam = queryParams.getOrDefault(QueryParameter.OPTIONS.key, emptyList()) + 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 = + if (entityRepresentation == EntityRepresentation.GEO_JSON) + queryParams.getFirst(QueryParameter.GEOMETRY_PROPERTY.key) ?: NGSILD_LOCATION_TERM + else null + val timeproperty = queryParams.getFirst(QueryParameter.TIMEPROPERTY.key) + + return NgsiLdDataRepresentation( + entityRepresentation, + attributeRepresentation, + includeSysAttrs, + languageFilter, + geometryProperty, + timeproperty + ) + } + } +} enum class AttributeRepresentation { NORMALIZED, diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/PaginationQuery.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/PaginationQuery.kt deleted file mode 100644 index 3c5166ff6..000000000 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/PaginationQuery.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.egm.stellio.shared.model - -data class PaginationQuery( - val offset: Int, - val limit: Int, - val count: Boolean = false -) diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/AllowedParameters.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/AllowedParameters.kt new file mode 100644 index 000000000..63413e39b --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/AllowedParameters.kt @@ -0,0 +1,64 @@ +package com.egm.stellio.shared.queryparameter + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import org.springframework.http.HttpStatus +import org.springframework.util.MultiValueMap +import kotlin.reflect.KClass + +@Target(AnnotationTarget.VALUE_PARAMETER,) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [ AllowedParameters.ParamValidator::class]) +annotation class AllowedParameters( + val implemented: Array = [], + val notImplemented: Array = [], + val message: String = "Invalid parameter received", + val groups: Array> = [], + val payload: Array> = [], +) { + class ParamValidator : ConstraintValidator> { + private var implemented: List = listOf() + private var notImplemented: List = listOf() + + override fun initialize(allowedParameters: AllowedParameters) { + this.implemented = allowedParameters.implemented.map(QueryParameter::key) + this.notImplemented = allowedParameters.notImplemented.map(QueryParameter::key) + } + + override fun isValid(params: MultiValueMap, context: ConstraintValidatorContext): Boolean { + if (implemented.containsAll(params.keys)) { + return true + } + + val notImplementedKeys = params.keys.intersect(notImplemented) + val invalidKeys = params.keys - notImplementedKeys - implemented + + context.disableDefaultConstraintViolation() + + val message = StringBuilder().apply { + if (notImplementedKeys.isNotEmpty()) { + append( + "The ['${notImplementedKeys.joinToString("', '")}'] parameters have not been implemented yet. " + ) + } + if (invalidKeys.isNotEmpty()) { + append( + "The ['${invalidKeys.joinToString("', '")}'] parameters are not allowed on this endpoint. " + ) + } + if (implemented.isNotEmpty()) + append("Accepted query parameters are '${implemented.joinToString("', '")}'. ") + else append("This endpoint does not accept any query parameters. ") + }.toString() + + context.buildConstraintViolationWithTemplate( + message + ).addPropertyNode( + if (notImplementedKeys.isEmpty()) HttpStatus.BAD_REQUEST.name else HttpStatus.NOT_IMPLEMENTED.name + ).addConstraintViolation() + + return false + } + } +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/GeoQuery.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/GeoQuery.kt new file mode 100644 index 000000000..bc505c7c7 --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/GeoQuery.kt @@ -0,0 +1,119 @@ +package com.egm.stellio.shared.queryparameter + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.BadRequestDataException +import com.egm.stellio.shared.model.ExpandedEntity +import com.egm.stellio.shared.model.ExpandedTerm +import com.egm.stellio.shared.model.WKTCoordinates +import com.egm.stellio.shared.util.JsonLdUtils +import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_VALUE +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY +import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm +import com.egm.stellio.shared.util.JsonUtils +import com.egm.stellio.shared.util.decode +import com.egm.stellio.shared.util.parseGeometryToWKT +import com.egm.stellio.shared.util.stringifyCoordinates + +data class GeoQuery( + val georel: String, + val geometry: GeometryType, + val coordinates: String, + val wktCoordinates: WKTCoordinates, + var geoproperty: ExpandedTerm = NGSILD_LOCATION_PROPERTY +) { + enum class GeometryType(val type: String) { + POINT("Point"), + MULTIPOINT("MultiPoint"), + LINESTRING("LineString"), + MULTILINESTRING("MultiLineString"), + POLYGON("Polygon"), + MULTIPOLYGON("MultiPolygon"); + + companion object { + fun isSupportedType(type: String): Boolean = + entries.any { it.type == type } + + fun forType(type: String): GeometryType? = + entries.find { it.type == type } + } + } + + fun buildSqlFilter(target: ExpandedEntity? = null): String { + val targetWKTCoordinates = + """ + (select jsonb_path_query_first(#{TARGET}#, '$."$geoproperty"."$NGSILD_GEOPROPERTY_VALUE"[0]')->>'$JSONLD_VALUE') + """.trimIndent() + val georelQuery = Georel.prepareQuery(georel) + + return ( + if (georelQuery.first == Georel.NEAR_DISTANCE_MODIFIER) + """ + public.ST_Distance( + cast('SRID=4326;${wktCoordinates.value}' as public.geography), + cast('SRID=4326;' || $targetWKTCoordinates as public.geography), + false + ) ${georelQuery.second} ${georelQuery.third} + """.trimIndent() + else + """ + public.ST_${georelQuery.first}( + public.ST_GeomFromText('${wktCoordinates.value}'), + public.ST_GeomFromText($targetWKTCoordinates) + ) + """.trimIndent() + ) + .let { + if (target == null) + it.replace("#{TARGET}#", "entity_payload.payload") + else + it.replace("#{TARGET}#", "'" + JsonUtils.serializeObject(target.members) + "'") + } + } + + companion object { + + fun parseGeoQueryParameters( + requestParams: Map, + contexts: List + ): Either = either { + val georel = requestParams[QueryParameter.GEOREL.key]?.decode()?.also { + Georel.verify(it).bind() + } + val geometry = requestParams[QueryParameter.GEOMETRY.key]?.let { + if (GeometryType.isSupportedType(it)) + GeometryType.forType(it).right() + else + BadRequestDataException("$it is not a recognized value for 'geometry' parameter").left() + }?.bind() + val coordinates = requestParams[QueryParameter.COORDINATES.key]?.decode()?.let { + stringifyCoordinates(it) + } + val geoproperty = requestParams[QueryParameter.GEOPROPERTY.key]?.let { + expandJsonLdTerm(it, contexts) + } ?: JsonLdUtils.NGSILD_LOCATION_PROPERTY + + // if at least one parameter is provided, the three must be provided for the geoquery to be valid + val notNullGeoParameters = listOfNotNull(georel, geometry, coordinates) + if (notNullGeoParameters.isEmpty()) + null + else if (georel == null || geometry == null || coordinates == null) + BadRequestDataException( + "Missing at least one geo parameter between 'geometry', 'georel' and 'coordinates'" + ) + .left().bind() + else + GeoQuery( + georel = georel, + geometry = geometry, + coordinates = coordinates, + wktCoordinates = parseGeometryToWKT(geometry, coordinates).bind(), + geoproperty = geoproperty + ) + } + } +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/Georel.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/Georel.kt new file mode 100644 index 000000000..d716f4ff9 --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/Georel.kt @@ -0,0 +1,42 @@ +package com.egm.stellio.shared.queryparameter + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.BadRequestDataException + +enum class Georel(val key: String) { + NEAR("near"), + WITHIN("within"), + CONTAINS("contains"), + INTERSECTS("intersects"), + EQUALS("equals"), + DISJOINT("disjoint"), + OVERLAPS("overlaps"); + companion object { + val ALL = entries.map { it.key } + + const val NEAR_DISTANCE_MODIFIER = "distance" + const val NEAR_MAXDISTANCE_MODIFIER = "maxDistance" + private val nearRegex = "^near;(?:minDistance|maxDistance)==\\d+$".toRegex() + + fun verify(georel: String): Either { + if (georel.startsWith(NEAR.key)) { + if (!georel.matches(nearRegex)) + return BadRequestDataException("Invalid expression for 'near' georel: $georel").left() + return Unit.right() + } else if (ALL.any { georel == it }) + return Unit.right() + else return BadRequestDataException("Invalid 'georel' parameter provided: $georel").left() + } + + fun prepareQuery(georel: String): Triple = + if (georel.startsWith(NEAR.key)) { + val comparisonParams = georel.split(";")[1].split("==") + if (comparisonParams[0] == NEAR_MAXDISTANCE_MODIFIER) + Triple(NEAR_DISTANCE_MODIFIER, "<=", comparisonParams[1]) + else Triple(NEAR_DISTANCE_MODIFIER, ">=", comparisonParams[1]) + } else Triple(georel, null, null) + } +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/LinkedEntityQuery.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/LinkedEntityQuery.kt new file mode 100644 index 000000000..96189ca44 --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/LinkedEntityQuery.kt @@ -0,0 +1,63 @@ +package com.egm.stellio.shared.queryparameter + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.BadRequestDataException +import com.egm.stellio.shared.util.toListOfUri +import java.net.URI +import kotlin.UInt + +data class LinkedEntityQuery( + val join: JoinType = JoinType.NONE, + val joinLevel: UInt = DEFAULT_JOIN_LEVEL.toUInt(), + val containedBy: Set = emptySet() +) { + companion object { + const val DEFAULT_JOIN_LEVEL = 1 + + enum class JoinType(val type: String) { + FLAT("flat"), + INLINE("inline"), + NONE("@none"); + + companion object { + fun forType(type: String): JoinType? = + entries.find { it.type == type } + } + } + + fun parseLinkedEntityQueryParameters( + join: String?, + joinLevel: String?, + containedBy: String? + ): Either = either { + val containedBy = containedBy?.split(",").orEmpty().toListOfUri().toSet() + val join = join?.let { + JoinType.forType(it)?.right() ?: BadRequestDataException(badJoinParameterMessage(it)).left() + }?.bind() + val joinLevel = joinLevel?.let { param -> + runCatching { + param.toUInt() + }.fold( + { it.right() }, + { + BadRequestDataException(badJoinLevelParameterMessage(param)).left() + } + ) + }?.bind() + + if ((joinLevel != null || containedBy.isNotEmpty()) && join == null) + raise(BadRequestDataException("'join' must be specified if 'joinLevel' or 'containedBy' are specified")) + else join?.let { LinkedEntityQuery(it, joinLevel ?: DEFAULT_JOIN_LEVEL.toUInt(), containedBy) } + } + + private fun badJoinParameterMessage(param: String) = + "'$param' is not a recognized value for 'join' parameter (only 'flat', 'inline' and '@none' are allowed)" + + private fun badJoinLevelParameterMessage(param: String) = + "'$param' is not a recognized value for 'joinLevel' parameter (only positive integers are allowed)" + } +} 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 new file mode 100644 index 000000000..283e13640 --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/OptionsValue.kt @@ -0,0 +1,7 @@ +package com.egm.stellio.shared.queryparameter + +enum class OptionsValue(val value: String) { + SYS_ATTRS("sysAttrs"), + KEY_VALUES("keyValues"), + NO_OVERWRITE("noOverwrite") +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/PaginationQuery.kt b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/PaginationQuery.kt new file mode 100644 index 000000000..5cb2955c1 --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/PaginationQuery.kt @@ -0,0 +1,39 @@ +package com.egm.stellio.shared.queryparameter + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.egm.stellio.shared.model.APIException +import com.egm.stellio.shared.model.BadRequestDataException +import com.egm.stellio.shared.model.TooManyResultsException +import org.springframework.util.MultiValueMap + +data class PaginationQuery( + val offset: Int, + val limit: Int, + val count: Boolean = false +) { + companion object { + + fun parsePaginationParameters( + queryParams: MultiValueMap, + limitDefault: Int, + limitMax: Int + ): Either { + val count = queryParams.getFirst(QueryParameter.COUNT.key)?.toBoolean() == true + val offset = queryParams.getFirst(QueryParameter.OFFSET.key)?.toIntOrNull() ?: 0 + val limit = queryParams.getFirst(QueryParameter.LIMIT.key)?.toIntOrNull() ?: limitDefault + if (!count && (limit <= 0 || offset < 0)) + return BadRequestDataException( + "Offset must be greater than zero and limit must be strictly greater than zero" + ).left() + if (count && (limit < 0 || offset < 0)) + return BadRequestDataException("Offset and limit must be greater than zero").left() + if (limit > limitMax) + return TooManyResultsException( + "You asked for $limit results, but the supported maximum limit is $limitMax" + ).left() + return PaginationQuery(offset, limit, count).right() + } + } +} 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 new file mode 100644 index 000000000..00d91392b --- /dev/null +++ b/shared/src/main/kotlin/com/egm/stellio/shared/queryparameter/QueryParameter.kt @@ -0,0 +1,61 @@ +package com.egm.stellio.shared.queryparameter + +typealias QP = QueryParameter + +enum class QueryParameter( + val key: String, +) { + ID("id"), + TYPE("type"), + ID_PATTERN("idPattern"), + ATTRS("attrs"), + Q("q"), + SCOPEQ("scopeQ"), + GEOMETRY_PROPERTY("geometryProperty"), + LANG("lang"), + DATASET_ID("datasetId"), + CONTAINED_BY("containedBy"), + JOIN("join"), + JOIN_LEVEL("joinLevel"), + OPTIONS("options"), + OBSERVED_AT("observedAt"), + + // geoQuery + GEOREL("georel"), + GEOMETRY("geometry"), + COORDINATES("coordinates"), + GEOPROPERTY("geoproperty"), + + // temporal + TIMEREL("timerel"), + TIMEAT("timeAt"), + ENDTIMEAT("endTimeAt"), + AGGRPERIODDURATION("aggrPeriodDuration"), + AGGRMETHODS("aggrMethods"), + LASTN("lastN"), + TIMEPROPERTY("timeproperty"), + + // pagination + COUNT("count"), + OFFSET("offset"), + LIMIT("limit"), + + DELETE_ALL("deleteAll"), + + // not implemented yet + FORMAT("format"), + PICK("pick"), + OMIT("omit"), + EXPAND_VALUES("expandValues"), + CSF("csf"), + ENTITY_MAP("entityMap"), + DETAILS("details"), + + // 6.3.18 limiting distributed operations + LOCAL("local"), + VIA("Via"); + + override fun toString(): String { + return key + } +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt index dba87e30b..e05b2391d 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiResponses.kt @@ -1,7 +1,7 @@ package com.egm.stellio.shared.util import com.egm.stellio.shared.model.BadRequestDataException -import com.egm.stellio.shared.model.PaginationQuery +import com.egm.stellio.shared.queryparameter.PaginationQuery import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY import com.egm.stellio.shared.util.JsonUtils.serializeObject import org.slf4j.LoggerFactory 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 174df29b3..8f8c8176d 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 @@ -7,24 +7,17 @@ import arrow.core.raise.ensure import arrow.core.right import arrow.fx.coroutines.parMap import com.egm.stellio.shared.model.APIException -import com.egm.stellio.shared.model.AttributeRepresentation import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.CompactedEntity -import com.egm.stellio.shared.model.EntityRepresentation import com.egm.stellio.shared.model.EntityTypeSelection -import com.egm.stellio.shared.model.NgsiLdDataRepresentation import com.egm.stellio.shared.model.NotAcceptableException -import com.egm.stellio.shared.model.PaginationQuery -import com.egm.stellio.shared.model.TooManyResultsException 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.JsonLdUtils.NGSILD_LOCATION_TERM import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.util.MimeTypeUtils -import org.springframework.util.MultiValueMap import reactor.core.publisher.Mono import java.time.ZonedDateTime import java.time.format.DateTimeParseException @@ -35,26 +28,7 @@ const val RESULTS_COUNT_HEADER = "NGSILD-Results-Count" const val JSON_LD_CONTENT_TYPE = "application/ld+json" const val GEO_JSON_CONTENT_TYPE = "application/geo+json" const val JSON_MERGE_PATCH_CONTENT_TYPE = "application/merge-patch+json" -const val QUERY_PARAM_COUNT: String = "count" -const val QUERY_PARAM_OFFSET: String = "offset" -const val QUERY_PARAM_LIMIT: String = "limit" -const val QUERY_PARAM_ID: String = "id" -const val QUERY_PARAM_TYPE: String = "type" -const val QUERY_PARAM_ID_PATTERN: String = "idPattern" -const val QUERY_PARAM_ATTRS: String = "attrs" -const val QUERY_PARAM_Q: String = "q" -const val QUERY_PARAM_SCOPEQ: String = "scopeQ" -const val QUERY_PARAM_GEOMETRY_PROPERTY: String = "geometryProperty" -const val QUERY_PARAM_LANG: String = "lang" -const val QUERY_PARAM_DATASET_ID: String = "datasetId" -const val QUERY_PARAM_OPTIONS: String = "options" -const val QUERY_PARAM_OPTIONS_SYSATTRS_VALUE: String = "sysAttrs" -const val QUERY_PARAM_OPTIONS_KEYVALUES_VALUE: String = "keyValues" -const val QUERY_PARAM_OPTIONS_NOOVERWRITE_VALUE: String = "noOverwrite" -const val QUERY_PARAM_OPTIONS_OBSERVEDAT_VALUE: String = "observedAt" -const val QUERY_PARAM_CONTAINED_BY: String = "containedBy" -const val QUERY_PARAM_JOIN: String = "join" -const val QUERY_PARAM_JOIN_LEVEL: String = "joinLevel" + val JSON_LD_MEDIA_TYPE = MediaType.valueOf(JSON_LD_CONTENT_TYPE) val GEO_JSON_MEDIA_TYPE = MediaType.valueOf(GEO_JSON_CONTENT_TYPE) @@ -206,8 +180,8 @@ fun hasValueInOptionsParam(options: Optional, optionValue: OptionsParamV .filter { it.any { option -> option == optionValue.value } } .isPresent -fun parseRequestParameter(requestParam: String?): Set = - requestParam +fun parseQueryParameter(queryParam: String?): Set = + queryParam ?.split(",") .orEmpty() .toSet() @@ -222,38 +196,12 @@ fun compactTypeSelection(entityTypeSelection: EntityTypeSelection, contexts: Lis JsonLdUtils.compactTerm(it.value.trim(), contexts) } -fun parseAndExpandRequestParameter(requestParam: String?, contexts: List): Set = - parseRequestParameter(requestParam) +fun parseAndExpandQueryParameter(queryParam: String?, contexts: List): Set = + parseQueryParameter(queryParam) .map { JsonLdUtils.expandJsonLdTerm(it.trim(), contexts) }.toSet() -fun parseRepresentations( - requestParams: MultiValueMap, - acceptMediaType: MediaType -): NgsiLdDataRepresentation { - val optionsParam = requestParams.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) - val includeSysAttrs = optionsParam.contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) - val attributeRepresentation = optionsParam.contains(QUERY_PARAM_OPTIONS_KEYVALUES_VALUE) - .let { if (it) AttributeRepresentation.SIMPLIFIED else AttributeRepresentation.NORMALIZED } - val languageFilter = requestParams.getFirst(QUERY_PARAM_LANG) - val entityRepresentation = EntityRepresentation.forMediaType(acceptMediaType) - val geometryProperty = - if (entityRepresentation == EntityRepresentation.GEO_JSON) - requestParams.getFirst(QUERY_PARAM_GEOMETRY_PROPERTY) ?: NGSILD_LOCATION_TERM - else null - val timeproperty = requestParams.getFirst("timeproperty") - - return NgsiLdDataRepresentation( - entityRepresentation, - attributeRepresentation, - includeSysAttrs, - languageFilter, - geometryProperty, - timeproperty - ) -} - fun validateIdPattern(idPattern: String?): Either = idPattern?.let { runCatching { @@ -264,27 +212,6 @@ fun validateIdPattern(idPattern: String?): Either = ) } ?: Either.Right(null) -fun parsePaginationParameters( - queryParams: MultiValueMap, - limitDefault: Int, - limitMax: Int -): Either { - val count = queryParams.getFirst(QUERY_PARAM_COUNT)?.toBoolean() ?: false - val offset = queryParams.getFirst(QUERY_PARAM_OFFSET)?.toIntOrNull() ?: 0 - val limit = queryParams.getFirst(QUERY_PARAM_LIMIT)?.toIntOrNull() ?: limitDefault - if (!count && (limit <= 0 || offset < 0)) - return BadRequestDataException( - "Offset must be greater than zero and limit must be strictly greater than zero" - ).left() - if (count && (limit < 0 || offset < 0)) - return BadRequestDataException("Offset and limit must be greater than zero").left() - if (limit > limitMax) - return TooManyResultsException( - "You asked for $limit results, but the supported maximum limit is $limitMax" - ).left() - return PaginationQuery(offset, limit, count).right() -} - fun getApplicableMediaType(httpHeaders: HttpHeaders): Either = httpHeaders.accept.getApplicable() diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/GeoQueryUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/GeoQueryUtils.kt deleted file mode 100644 index af4fd7313..000000000 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/GeoQueryUtils.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.egm.stellio.shared.util - -import arrow.core.Either -import arrow.core.left -import arrow.core.raise.either -import arrow.core.right -import com.egm.stellio.shared.model.APIException -import com.egm.stellio.shared.model.BadRequestDataException -import com.egm.stellio.shared.model.ExpandedEntity -import com.egm.stellio.shared.model.GeoQuery -import com.egm.stellio.shared.model.WKTCoordinates -import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_VALUE -import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm - -const val GEO_QUERY_PARAM_GEOREL = "georel" -const val GEO_QUERY_PARAM_GEOMETRY = "geometry" -const val GEO_QUERY_PARAM_COORDINATES = "coordinates" -const val GEO_QUERY_PARAM_GEOPROPERTY = "geoproperty" - -const val GEO_QUERY_GEOREL_NEAR = "near" -const val GEO_QUERY_GEOREL_WITHIN = "within" -const val GEO_QUERY_GEOREL_CONTAINS = "contains" -const val GEO_QUERY_GEOREL_INTERSECTS = "intersects" -const val GEO_QUERY_GEOREL_EQUALS = "equals" -const val GEO_QUERY_GEOREL_DISJOINT = "disjoint" -const val GEO_QUERY_GEOREL_OVERLAPS = "overlaps" -val GEO_QUERY_ALL_GEORELS = listOf( - GEO_QUERY_GEOREL_NEAR, - GEO_QUERY_GEOREL_WITHIN, - GEO_QUERY_GEOREL_CONTAINS, - GEO_QUERY_GEOREL_INTERSECTS, - GEO_QUERY_GEOREL_EQUALS, - GEO_QUERY_GEOREL_DISJOINT, - GEO_QUERY_GEOREL_OVERLAPS -) -const val GEOREL_NEAR_DISTANCE_MODIFIER = "distance" -const val GEOREL_NEAR_MAXDISTANCE_MODIFIER = "maxDistance" - -private val georelNearRegex = "^near;(?:minDistance|maxDistance)==\\d+$".toRegex() - -fun parseGeoQueryParameters( - requestParams: Map, - contexts: List -): Either = either { - val georel = requestParams[GEO_QUERY_PARAM_GEOREL]?.decode()?.also { - checkGeorelParam(it).bind() - } - val geometry = requestParams[GEO_QUERY_PARAM_GEOMETRY]?.let { - if (GeoQuery.GeometryType.isSupportedType(it)) - GeoQuery.GeometryType.forType(it).right() - else - BadRequestDataException("$it is not a recognized value for 'geometry' parameter").left() - }?.bind() - val coordinates = requestParams[GEO_QUERY_PARAM_COORDINATES]?.decode()?.let { - stringifyCoordinates(it) - } - val geoproperty = requestParams[GEO_QUERY_PARAM_GEOPROPERTY]?.let { - expandJsonLdTerm(it, contexts) - } ?: JsonLdUtils.NGSILD_LOCATION_PROPERTY - - // if at least one parameter is provided, the three must be provided for the geoquery to be valid - val notNullGeoParameters = listOfNotNull(georel, geometry, coordinates) - if (notNullGeoParameters.isEmpty()) - null - else if (georel == null || geometry == null || coordinates == null) - BadRequestDataException("Missing at least one geo parameter between 'geometry', 'georel' and 'coordinates'") - .left().bind() - else - GeoQuery( - georel = georel, - geometry = geometry, - coordinates = coordinates, - wktCoordinates = parseGeometryToWKT(geometry, coordinates).bind(), - geoproperty = geoproperty - ) -} - -fun checkGeorelParam(georel: String): Either { - if (georel.startsWith(GEO_QUERY_GEOREL_NEAR)) { - if (!georel.matches(georelNearRegex)) - return BadRequestDataException("Invalid expression for 'near' georel: $georel").left() - return Unit.right() - } else if (GEO_QUERY_ALL_GEORELS.any { georel == it }) - return Unit.right() - else return BadRequestDataException("Invalid 'georel' parameter provided: $georel").left() -} - -fun stringifyCoordinates(coordinates: Any): String = - when (coordinates) { - is String -> coordinates - is List<*> -> coordinates.toString() - else -> coordinates.toString() - } - -fun parseGeometryToWKT( - geometryType: GeoQuery.GeometryType, - coordinates: String -): Either = - geoJsonToWkt(geometryType, coordinates) - -private fun prepareGeorelQuery(georel: String): Triple = - if (georel.startsWith(GEO_QUERY_GEOREL_NEAR)) { - val comparisonParams = georel.split(";")[1].split("==") - if (comparisonParams[0] == GEOREL_NEAR_MAXDISTANCE_MODIFIER) - Triple(GEOREL_NEAR_DISTANCE_MODIFIER, "<=", comparisonParams[1]) - else Triple(GEOREL_NEAR_DISTANCE_MODIFIER, ">=", comparisonParams[1]) - } else Triple(georel, null, null) - -fun buildGeoQuery(geoQuery: GeoQuery, target: ExpandedEntity? = null): String { - val targetWKTCoordinates = - """ - (select jsonb_path_query_first(#{TARGET}#, '$."${geoQuery.geoproperty}"."$NGSILD_GEOPROPERTY_VALUE"[0]')->>'$JSONLD_VALUE') - """.trimIndent() - val georelQuery = prepareGeorelQuery(geoQuery.georel) - - return ( - if (georelQuery.first == GEOREL_NEAR_DISTANCE_MODIFIER) - """ - public.ST_Distance( - cast('SRID=4326;${geoQuery.wktCoordinates.value}' as public.geography), - cast('SRID=4326;' || $targetWKTCoordinates as public.geography), - false - ) ${georelQuery.second} ${georelQuery.third} - """.trimIndent() - else - """ - public.ST_${georelQuery.first}( - public.ST_GeomFromText('${geoQuery.wktCoordinates.value}'), - public.ST_GeomFromText($targetWKTCoordinates) - ) - """.trimIndent() - ) - .let { - if (target == null) - it.replace("#{TARGET}#", "entity_payload.payload") - else - it.replace("#{TARGET}#", "'" + JsonUtils.serializeObject(target.members) + "'") - } -} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/GeoUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/GeoUtils.kt index 2d656ebc1..60f319469 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/GeoUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/GeoUtils.kt @@ -5,8 +5,8 @@ import arrow.core.left import arrow.core.right import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.BadRequestDataException -import com.egm.stellio.shared.model.GeoQuery import com.egm.stellio.shared.model.WKTCoordinates +import com.egm.stellio.shared.queryparameter.GeoQuery import com.egm.stellio.shared.util.JsonUtils.deserializeObject import com.egm.stellio.shared.util.JsonUtils.serializeObject import org.locationtech.jts.io.WKTReader @@ -54,3 +54,16 @@ fun wktToGeoJson(wkt: String): Map { geoJsonWriter.setEncodeCRS(false) return deserializeObject(geoJsonWriter.write(geometry)) } + +fun stringifyCoordinates(coordinates: Any): String = + when (coordinates) { + is String -> coordinates + is List<*> -> coordinates.toString() + else -> coordinates.toString() + } + +fun parseGeometryToWKT( + geometryType: GeoQuery.GeometryType, + coordinates: String +): Either = + geoJsonToWkt(geometryType, coordinates) diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/LinkedEntityQueryUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/LinkedEntityQueryUtils.kt deleted file mode 100644 index 30c5422a0..000000000 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/LinkedEntityQueryUtils.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.egm.stellio.shared.util - -import arrow.core.Either -import arrow.core.left -import arrow.core.raise.either -import arrow.core.right -import com.egm.stellio.shared.model.APIException -import com.egm.stellio.shared.model.BadRequestDataException -import com.egm.stellio.shared.model.LinkedEntityQuery -import com.egm.stellio.shared.model.LinkedEntityQuery.Companion.DEFAULT_JOIN_LEVEL -import com.egm.stellio.shared.model.LinkedEntityQuery.JoinType - -fun parseLinkedEntityQueryParameters( - join: String?, - joinLevel: String?, - containedBy: String? -): Either = either { - val containedBy = containedBy?.split(",").orEmpty().toListOfUri().toSet() - val join = join?.let { - JoinType.forType(it)?.right() - ?: BadRequestDataException( - "'$it' is not a recognized value for 'join' parameter (only 'flat', 'inline' and '@none' are allowed)" - ).left() - }?.bind() - val joinLevel = joinLevel?.let { param -> - runCatching { - param.toUInt() - }.fold( - { it.right() }, - { - BadRequestDataException( - "'$param' is not a recognized value for 'joinLevel' parameter (only positive integers are allowed)" - ).left() - } - ) - }?.bind() - - if ((joinLevel != null || containedBy.isNotEmpty()) && join == null) - raise(BadRequestDataException("'join' must be specified if 'joinLevel' or 'containedBy' are specified")) - else join?.let { LinkedEntityQuery(it, joinLevel ?: DEFAULT_JOIN_LEVEL.toUInt(), containedBy) } -} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt b/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt index 7b808ed48..dcac4ea34 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt @@ -3,11 +3,14 @@ package com.egm.stellio.shared.web import com.apicatalog.jsonld.JsonLdError import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.InternalErrorException +import com.egm.stellio.shared.model.InvalidRequestException import com.egm.stellio.shared.model.JsonLdErrorApiResponse import com.egm.stellio.shared.model.JsonParseApiException import com.egm.stellio.shared.model.NotAcceptableException +import com.egm.stellio.shared.model.NotImplementedException import com.egm.stellio.shared.model.UnsupportedMediaTypeStatusApiException import com.fasterxml.jackson.core.JsonParseException +import jakarta.validation.ConstraintViolationException import org.springframework.core.codec.CodecException import org.springframework.http.HttpStatus import org.springframework.http.ProblemDetail @@ -35,6 +38,15 @@ class ExceptionHandler { NotAcceptableException(cause.message).toErrorResponse() is MethodNotAllowedException -> ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(cause.body) + is ConstraintViolationException -> { + val message = cause.constraintViolations.joinToString(". ") { it.message } + if (cause.constraintViolations.flatMap { it.propertyPath } + .any { it.name == HttpStatus.NOT_IMPLEMENTED.name } + ) + NotImplementedException(message).toErrorResponse() + else + InvalidRequestException(message).toErrorResponse() + } else -> InternalErrorException("$cause").toErrorResponse() } diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedEntityTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedEntityTests.kt index d1a9a5ca5..2a1a6558d 100644 --- a/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedEntityTests.kt +++ b/shared/src/test/kotlin/com/egm/stellio/shared/model/ExpandedEntityTests.kt @@ -14,7 +14,7 @@ import com.egm.stellio.shared.util.assertJsonPayloadsAreEqual import com.egm.stellio.shared.util.expandJsonLdEntity import com.egm.stellio.shared.util.loadAndExpandSampleData import com.egm.stellio.shared.util.ngsiLdDateTime -import com.egm.stellio.shared.util.parseAndExpandRequestParameter +import com.egm.stellio.shared.util.parseAndExpandQueryParameter import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertEquals @@ -102,7 +102,7 @@ class ExpandedEntityTests { } """.trimIndent() - val attributesToMatch: Set = parseAndExpandRequestParameter("managedBy", listOf(APIC_COMPOUND_CONTEXT)) + val attributesToMatch: Set = parseAndExpandQueryParameter("managedBy", listOf(APIC_COMPOUND_CONTEXT)) val filteredEntity = ExpandedEntity(entity.filterAttributes(attributesToMatch, emptySet())) @@ -127,7 +127,7 @@ class ExpandedEntityTests { } """.trimIndent() - val attributesToMatch: Set = parseAndExpandRequestParameter("name", listOf(APIC_COMPOUND_CONTEXT)) + val attributesToMatch: Set = parseAndExpandQueryParameter("name", listOf(APIC_COMPOUND_CONTEXT)) val datasetIdToMatch: Set = setOf("urn:ngsi-ld:Dataset:english-name") val filteredEntity = ExpandedEntity(entity.filterAttributes(attributesToMatch, datasetIdToMatch)) val compactedEntity = compactEntity(filteredEntity, listOf(APIC_COMPOUND_CONTEXT)) @@ -203,7 +203,7 @@ class ExpandedEntityTests { } """.trimIndent() - val attributesToMatch: Set = parseAndExpandRequestParameter("name", listOf(APIC_COMPOUND_CONTEXT)) + val attributesToMatch: Set = parseAndExpandQueryParameter("name", listOf(APIC_COMPOUND_CONTEXT)) val filteredEntity = ExpandedEntity(entity.filterAttributes(attributesToMatch, setOf(NGSILD_NONE_TERM))) val compactedEntity = compactEntity(filteredEntity, listOf(APIC_COMPOUND_CONTEXT)) assertJsonPayloadsAreEqual(expectedEntity, serializeObject(compactedEntity)) @@ -259,7 +259,7 @@ class ExpandedEntityTests { } """.trimIndent() - val attributesToMatch: Set = parseAndExpandRequestParameter("name", listOf(APIC_COMPOUND_CONTEXT)) + val attributesToMatch: Set = parseAndExpandQueryParameter("name", listOf(APIC_COMPOUND_CONTEXT)) val datasetIdToMatch: Set = setOf("urn:ngsi-ld:Dataset:managedBy") val filteredEntity = ExpandedEntity(entity.filterAttributes(attributesToMatch, datasetIdToMatch)) val compactedEntity = compactEntity(filteredEntity, listOf(APIC_COMPOUND_CONTEXT)) 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 f6323f3f0..00a9da49a 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 @@ -58,17 +58,17 @@ class ApiUtilsTests { @Test fun `it should return an empty list if no attrs param is provided`() { - assertTrue(parseAndExpandRequestParameter(null, emptyList()).isEmpty()) + assertTrue(parseAndExpandQueryParameter(null, emptyList()).isEmpty()) } @Test fun `it should return an singleton list if there is one provided attrs param`() { - assertEquals(1, parseAndExpandRequestParameter("attr1", NGSILD_TEST_CORE_CONTEXTS).size) + assertEquals(1, parseAndExpandQueryParameter("attr1", NGSILD_TEST_CORE_CONTEXTS).size) } @Test fun `it should return a list with two elements if there are two provided attrs param`() { - assertEquals(2, parseAndExpandRequestParameter("attr1, attr2", NGSILD_TEST_CORE_CONTEXTS).size) + assertEquals(2, parseAndExpandQueryParameter("attr1, attr2", NGSILD_TEST_CORE_CONTEXTS).size) } @Test diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/util/GeoQueryUtilsTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/util/GeoQueryUtilsTests.kt index 2ea162b52..69340a16e 100644 --- a/shared/src/test/kotlin/com/egm/stellio/shared/util/GeoQueryUtilsTests.kt +++ b/shared/src/test/kotlin/com/egm/stellio/shared/util/GeoQueryUtilsTests.kt @@ -2,8 +2,9 @@ package com.egm.stellio.shared.util import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.ExpandedEntity -import com.egm.stellio.shared.model.GeoQuery -import com.egm.stellio.shared.model.GeoQuery.GeometryType +import com.egm.stellio.shared.queryparameter.GeoQuery +import com.egm.stellio.shared.queryparameter.GeoQuery.Companion.parseGeoQueryParameters +import com.egm.stellio.shared.queryparameter.GeoQuery.GeometryType import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OPERATION_SPACE_PROPERTY import kotlinx.coroutines.test.runTest @@ -139,7 +140,7 @@ class GeoQueryUtilsTests { ) val expandedEntity = gimmeSimpleEntityWithGeoProperty("location", 24.30623, 60.07966) - val queryStatement = buildGeoQuery(geoQuery, expandedEntity) + val queryStatement = geoQuery.buildSqlFilter(expandedEntity) assertEqualsIgnoringNoise( """ @@ -163,7 +164,7 @@ class GeoQueryUtilsTests { ) val expandedEntity = gimmeSimpleEntityWithGeoProperty("location", 60.07966, 24.30623) - val queryStatement = buildGeoQuery(geoQuery, expandedEntity) + val queryStatement = geoQuery.buildSqlFilter(expandedEntity) assertEqualsIgnoringNoise( """ @@ -187,7 +188,7 @@ class GeoQueryUtilsTests { ) val expandedEntity = gimmeSimpleEntityWithGeoProperty("location", 60.30623, 30.07966) - val queryStatement = buildGeoQuery(geoQuery, expandedEntity) + val queryStatement = geoQuery.buildSqlFilter(expandedEntity) assertEqualsIgnoringNoise( """ diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJob.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJob.kt index 780945dd8..cf1f29bb7 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJob.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJob.kt @@ -4,12 +4,8 @@ import arrow.core.flatten import com.egm.stellio.shared.config.ApplicationProperties import com.egm.stellio.shared.model.CompactedEntity import com.egm.stellio.shared.model.EntitySelector +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.JsonUtils -import com.egm.stellio.shared.util.QUERY_PARAM_ATTRS -import com.egm.stellio.shared.util.QUERY_PARAM_ID -import com.egm.stellio.shared.util.QUERY_PARAM_ID_PATTERN -import com.egm.stellio.shared.util.QUERY_PARAM_Q -import com.egm.stellio.shared.util.QUERY_PARAM_TYPE import com.egm.stellio.shared.util.encode import com.egm.stellio.shared.web.NGSILD_TENANT_HEADER import com.egm.stellio.subscription.model.Notification @@ -57,12 +53,13 @@ class TimeIntervalNotificationJob( fun prepareQueryParams(entitySelector: EntitySelector, q: String?, attributes: List?): String { val param = java.lang.StringBuilder() - param.append("?$QUERY_PARAM_TYPE=${entitySelector.typeSelection.encode()}") - if (entitySelector.id != null) param.append("&$QUERY_PARAM_ID=${entitySelector.id}") - if (entitySelector.idPattern != null) param.append("&$QUERY_PARAM_ID_PATTERN=${entitySelector.idPattern}") - if (q != null) param.append("&$QUERY_PARAM_Q=${q.encode()}") + param.append("?${QueryParameter.TYPE.key}=${entitySelector.typeSelection.encode()}") + if (entitySelector.id != null) param.append("&${QueryParameter.ID.key}=${entitySelector.id}") + if (entitySelector.idPattern != null) + param.append("&${QueryParameter.ID_PATTERN.key}=${entitySelector.idPattern}") + if (q != null) param.append("&${QueryParameter.Q.key}=${q.encode()}") if (!attributes.isNullOrEmpty()) - param.append("&$QUERY_PARAM_ATTRS=${attributes.joinToString(",") { it.encode() }}") + param.append("&${QueryParameter.ATTRS.key}=${attributes.joinToString(",") { it.encode() }}") return param.toString() } diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/model/GeoQ.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/model/GeoQ.kt index 3851c9b7b..75426b2e8 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/model/GeoQ.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/model/GeoQ.kt @@ -1,9 +1,6 @@ package com.egm.stellio.subscription.model -import com.egm.stellio.shared.util.GEO_QUERY_PARAM_COORDINATES -import com.egm.stellio.shared.util.GEO_QUERY_PARAM_GEOMETRY -import com.egm.stellio.shared.util.GEO_QUERY_PARAM_GEOPROPERTY -import com.egm.stellio.shared.util.GEO_QUERY_PARAM_GEOREL +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY import com.fasterxml.jackson.annotation.JsonIgnore import org.springframework.data.relational.core.mapping.Table @@ -20,9 +17,9 @@ data class GeoQ( // representation passed to function checking for the correctness of geo-queries fun toMap(): Map = mapOf( - GEO_QUERY_PARAM_GEOREL to georel, - GEO_QUERY_PARAM_GEOMETRY to geometry, - GEO_QUERY_PARAM_COORDINATES to coordinates, - GEO_QUERY_PARAM_GEOPROPERTY to geoproperty + QueryParameter.GEOREL.key to georel, + QueryParameter.GEOMETRY.key to geometry, + QueryParameter.COORDINATES.key to coordinates, + QueryParameter.GEOPROPERTY.key to geoproperty ) } diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt index 5c2a75b3f..bb27259b4 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt @@ -10,9 +10,10 @@ import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.EntitySelector import com.egm.stellio.shared.model.ExpandedEntity import com.egm.stellio.shared.model.ExpandedTerm -import com.egm.stellio.shared.model.GeoQuery import com.egm.stellio.shared.model.NotImplementedException import com.egm.stellio.shared.model.WKTCoordinates +import com.egm.stellio.shared.queryparameter.GeoQuery +import com.egm.stellio.shared.queryparameter.GeoQuery.Companion.parseGeoQueryParameters import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE_TERM import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY @@ -21,14 +22,12 @@ import com.egm.stellio.shared.util.JsonLdUtils.checkJsonldContext import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm import com.egm.stellio.shared.util.Sub import com.egm.stellio.shared.util.buildContextLinkHeader -import com.egm.stellio.shared.util.buildGeoQuery import com.egm.stellio.shared.util.buildQQuery import com.egm.stellio.shared.util.buildScopeQQuery import com.egm.stellio.shared.util.buildTypeQuery import com.egm.stellio.shared.util.decode import com.egm.stellio.shared.util.invalidUriMessage import com.egm.stellio.shared.util.ngsiLdDateTime -import com.egm.stellio.shared.util.parseGeoQueryParameters import com.egm.stellio.shared.util.toStringValue import com.egm.stellio.subscription.config.SubscriptionProperties import com.egm.stellio.subscription.model.Endpoint @@ -673,16 +672,13 @@ class SubscriptionService( expandedEntity: ExpandedEntity ): String? = geoQ?.let { - buildGeoQuery( - GeoQuery( - georel = geoQ.georel, - geometry = GeoQuery.GeometryType.forType(geoQ.geometry)!!, - coordinates = geoQ.coordinates, - geoproperty = geoQ.geoproperty, - wktCoordinates = WKTCoordinates(geoQ.pgisGeometry!!) - ), - expandedEntity - ) + GeoQuery( + georel = geoQ.georel, + geometry = GeoQuery.GeometryType.forType(geoQ.geometry)!!, + coordinates = geoQ.coordinates, + geoproperty = geoQ.geoproperty, + wktCoordinates = WKTCoordinates(geoQ.pgisGeometry!!) + ).buildSqlFilter(expandedEntity) } suspend fun updateSubscriptionNotification( diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt index c073a4945..05cad29c8 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt @@ -11,20 +11,22 @@ import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.AlreadyExistsException import com.egm.stellio.shared.model.ResourceNotFoundException +import com.egm.stellio.shared.queryparameter.AllowedParameters +import com.egm.stellio.shared.queryparameter.OptionsValue +import com.egm.stellio.shared.queryparameter.PaginationQuery.Companion.parsePaginationParameters +import com.egm.stellio.shared.queryparameter.QP +import com.egm.stellio.shared.queryparameter.QueryParameter import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JSON_MERGE_PATCH_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_CONTEXT import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap import com.egm.stellio.shared.util.JsonUtils.serializeObject -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS -import com.egm.stellio.shared.util.QUERY_PARAM_OPTIONS_SYSATTRS_VALUE import com.egm.stellio.shared.util.Sub import com.egm.stellio.shared.util.buildQueryResponse import com.egm.stellio.shared.util.checkAndGetContext import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.getSubFromSecurityContext -import com.egm.stellio.shared.util.parsePaginationParameters import com.egm.stellio.shared.util.prepareGetSuccessResponseHeaders import com.egm.stellio.shared.web.BaseHandler import com.egm.stellio.subscription.model.Subscription @@ -37,6 +39,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.util.MultiValueMap +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping @@ -49,10 +52,10 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.net.URI -import java.util.Optional @RestController @RequestMapping("/ngsi-ld/v1/subscriptions") +@Validated class SubscriptionHandler( private val applicationProperties: ApplicationProperties, private val subscriptionService: SubscriptionService @@ -64,7 +67,9 @@ class SubscriptionHandler( @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun create( @RequestHeader httpHeaders: HttpHeaders, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(notImplemented = [QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val body = requestBody.awaitFirst().deserializeAsMap() val contexts = checkAndGetContext(httpHeaders, body, applicationProperties.contexts.core).bind() @@ -89,16 +94,20 @@ class SubscriptionHandler( @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getSubscriptions( @RequestHeader httpHeaders: HttpHeaders, - @RequestParam params: MultiValueMap + @AllowedParameters( + implemented = [QP.OPTIONS, QP.LIMIT, QP.OFFSET, QP.COUNT], + notImplemented = [QP.VIA] + ) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() val sub = getSubFromSecurityContext() - val includeSysAttrs = params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()) - .contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) + val includeSysAttrs = queryParams.getOrDefault(QueryParameter.OPTIONS.key, emptyList()) + .contains(OptionsValue.SYS_ATTRS.value) val paginationQuery = parsePaginationParameters( - params, + queryParams, applicationProperties.pagination.limitDefault, applicationProperties.pagination.limitMax ).bind() @@ -111,7 +120,7 @@ class SubscriptionHandler( subscriptionsCount, "/ngsi-ld/v1/subscriptions", paginationQuery, - params, + queryParams, mediaType, contexts ) @@ -127,9 +136,13 @@ class SubscriptionHandler( suspend fun getByURI( @RequestHeader httpHeaders: HttpHeaders, @PathVariable subscriptionId: URI, - @RequestParam options: Optional + @AllowedParameters(implemented = [QP.OPTIONS], notImplemented = [QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { - val includeSysAttrs = options.filter { it.contains(QUERY_PARAM_OPTIONS_SYSATTRS_VALUE) }.isPresent + val options: String? = queryParams.getFirst(QP.OPTIONS.key) // list of options see 6.3.11 + + val includeSysAttrs = options?.contains(OptionsValue.SYS_ATTRS.value) ?: false + val contexts = getContextFromLinkHeaderOrDefault(httpHeaders, applicationProperties.contexts.core).bind() val mediaType = getApplicableMediaType(httpHeaders).bind() @@ -172,7 +185,9 @@ class SubscriptionHandler( suspend fun update( @PathVariable subscriptionId: URI, @RequestHeader httpHeaders: HttpHeaders, - @RequestBody requestBody: Mono + @RequestBody requestBody: Mono, + @AllowedParameters(notImplemented = [QP.VIA]) + @RequestParam queryParams: MultiValueMap ): ResponseEntity<*> = either { checkSubscriptionExists(subscriptionId).bind() @@ -192,7 +207,11 @@ class SubscriptionHandler( * Implements 6.11.3.3 - Delete Subscription */ @DeleteMapping("/{subscriptionId}") - suspend fun delete(@PathVariable subscriptionId: URI): ResponseEntity<*> = either { + suspend fun delete( + @PathVariable subscriptionId: URI, + @AllowedParameters(notImplemented = [QP.VIA]) + @RequestParam queryParams: MultiValueMap + ): ResponseEntity<*> = either { checkSubscriptionExists(subscriptionId).bind() val sub = getSubFromSecurityContext()