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()