diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt
index 3e97bb9e66..22973ec261 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityAccessControlHandler.kt
@@ -75,10 +75,7 @@ class EntityAccessControlHandler(
mediaType
)
- val ngsiLdDataRepresentation = parseRepresentations(
- params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()),
- mediaType
- )
+ val ngsiLdDataRepresentation = parseRepresentations(params, mediaType)
buildQueryResponse(
compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation),
countAndAuthorizedEntities.first,
@@ -126,10 +123,7 @@ class EntityAccessControlHandler(
mediaType
)
- val ngsiLdDataRepresentation = parseRepresentations(
- params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()),
- mediaType
- )
+ val ngsiLdDataRepresentation = parseRepresentations(params, mediaType)
buildQueryResponse(
compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation),
countAndGroupEntities.first,
@@ -178,10 +172,7 @@ class EntityAccessControlHandler(
mediaType
)
- val ngsiLdDataRepresentation = parseRepresentations(
- params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()),
- mediaType
- )
+ val ngsiLdDataRepresentation = parseRepresentations(params, mediaType)
buildQueryResponse(
compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation),
countAndUserEntities.first,
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt
index 3adeed884d..d4eb5869f6 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt
@@ -208,10 +208,7 @@ class EntityHandler(
mediaType
)
- val ngsiLdDataRepresentation = parseRepresentations(
- params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()),
- mediaType
- )
+ val ngsiLdDataRepresentation = parseRepresentations(params, mediaType)
buildQueryResponse(
compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation),
countAndEntities.second,
@@ -259,10 +256,7 @@ class EntityHandler(
)
val compactedEntity = JsonLdUtils.compactEntity(filteredJsonLdEntity, contextLink, mediaType).toMutableMap()
- val ngsiLdDataRepresentation = parseRepresentations(
- params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()),
- mediaType
- )
+ val ngsiLdDataRepresentation = parseRepresentations(params, mediaType)
prepareGetSuccessResponse(mediaType, contextLink)
.body(serializeObject(compactedEntity.toFinalRepresentation(ngsiLdDataRepresentation)))
}.fold(
diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt
index 02bcc39dab..73e77551ea 100644
--- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt
+++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityOperationHandler.kt
@@ -281,10 +281,7 @@ class EntityOperationHandler(
mediaType
)
- val ngsiLdDataRepresentation = parseRepresentations(
- params.getOrDefault(QUERY_PARAM_OPTIONS, emptyList()),
- mediaType
- )
+ val ngsiLdDataRepresentation = parseRepresentations(params, mediaType)
buildQueryResponse(
compactedEntities.toFinalRepresentation(ngsiLdDataRepresentation),
countAndEntities.second,
diff --git a/shared/config/detekt/baseline.xml b/shared/config/detekt/baseline.xml
index 2faea8f5c5..aa4e5a0a6e 100644
--- a/shared/config/detekt/baseline.xml
+++ b/shared/config/detekt/baseline.xml
@@ -3,9 +3,10 @@
CyclomaticComplexMethod:ExceptionHandler.kt$ExceptionHandler$@ExceptionHandler fun transformErrorResponse(throwable: Throwable): ResponseEntity<ProblemDetail>
+ LongMethod:JsonLdEntityTests.kt$JsonLdEntityTests$@Test fun `it should return simplified GeoJSON entities`()
LongMethod:QueryUtils.kt$private fun transformQQueryToSqlJsonPath( mainAttributePath: List<ExpandedTerm>, trailingAttributePath: List<ExpandedTerm>, operator: String, value: String )
LongParameterList:ApiResponses.kt$( body: String, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap<String, String>, mediaType: MediaType, contextLink: String )
- LongParameterList:ApiResponses.kt$( entities: List<CompactedJsonLdEntity>, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap<String, String>, mediaType: MediaType, contextLink: String )
+ LongParameterList:ApiResponses.kt$( entities: Any, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap<String, String>, mediaType: MediaType, contextLink: String )
LongParameterList:NgsiLdEntity.kt$NgsiLdEntity$( val id: URI, val types: List<ExpandedTerm>, val scopes: List<String>?, val relationships: List<NgsiLdRelationship>, val properties: List<NgsiLdProperty>, val geoProperties: List<NgsiLdGeoProperty>, val contexts: List<String> )
LongParameterList:NgsiLdEntity.kt$NgsiLdGeoPropertyInstance$( val coordinates: WKTCoordinates, createdAt: ZonedDateTime?, modifiedAt: ZonedDateTime?, observedAt: ZonedDateTime?, datasetId: URI?, properties: List<NgsiLdProperty>, relationships: List<NgsiLdRelationship> )
LongParameterList:NgsiLdEntity.kt$NgsiLdPropertyInstance$( val value: Any, val unitCode: String?, createdAt: ZonedDateTime?, modifiedAt: ZonedDateTime?, observedAt: ZonedDateTime?, datasetId: URI?, properties: List<NgsiLdProperty>, relationships: List<NgsiLdRelationship> )
diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt
index c8cade7873..556a100376 100644
--- a/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt
+++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt
@@ -5,7 +5,10 @@ import arrow.core.left
import arrow.core.right
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID_TERM
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE_TERM
+import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.castAttributeValue
@@ -114,6 +117,21 @@ private fun simplifyValue(value: Map): Any {
}
}
+fun CompactedJsonLdEntity.toGeoJson(geometryProperty: String): Any {
+ val geometryAttributeContent = this[geometryProperty] as? Map
+ val geometryPropertyValue = geometryAttributeContent?.let {
+ if (it.containsKey(JSONLD_VALUE_TERM)) it[JSONLD_VALUE_TERM]
+ else it
+ }
+
+ return mapOf(
+ JSONLD_ID_TERM to this[JSONLD_ID_TERM]!!,
+ JSONLD_TYPE_TERM to FEATURE_TYPE,
+ GEOMETRY_PROPERTY_TERM to geometryPropertyValue,
+ PROPERTIES_PROPERTY_TERM to this.filter { it.key != JSONLD_ID_TERM }
+ )
+}
+
fun CompactedJsonLdEntity.withoutSysAttrs(): Map =
this.filter {
!JsonLdUtils.NGSILD_SYSATTRS_TERMS.contains(it.key)
@@ -133,18 +151,30 @@ fun CompactedJsonLdEntity.withoutSysAttrs(): Map =
fun CompactedJsonLdEntity.toFinalRepresentation(
ngsiLdDataRepresentation: NgsiLdDataRepresentation
-): CompactedJsonLdEntity =
+): Any =
this.let {
if (!ngsiLdDataRepresentation.includeSysAttrs) it.withoutSysAttrs()
else it
}.let {
if (ngsiLdDataRepresentation.attributeRepresentation == AttributeRepresentation.SIMPLIFIED) it.toKeyValues()
else it
+ }.let {
+ if (ngsiLdDataRepresentation.entityRepresentation == EntityRepresentation.GEO_JSON)
+ // geometryProperty is not null when GeoJSON representation is asked (defaults to location)
+ it.toGeoJson(ngsiLdDataRepresentation.geometryProperty!!)
+ else it
}
fun List.toFinalRepresentation(
ngsiLdDataRepresentation: NgsiLdDataRepresentation
-): List =
+): Any =
this.map {
it.toFinalRepresentation(ngsiLdDataRepresentation)
+ }.let {
+ if (ngsiLdDataRepresentation.entityRepresentation == EntityRepresentation.GEO_JSON) {
+ mapOf(
+ JSONLD_TYPE_TERM to FEATURE_COLLECTION_TYPE,
+ FEATURES_PROPERTY_TERM to it
+ )
+ } else it
}
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 b679681d8e..8eb1a5619f 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
@@ -10,7 +10,10 @@ import org.springframework.http.MediaType
data class NgsiLdDataRepresentation(
val entityRepresentation: EntityRepresentation,
val attributeRepresentation: AttributeRepresentation,
- val includeSysAttrs: Boolean
+ val includeSysAttrs: Boolean,
+ // In the case of GeoJSON Entity representation,
+ // this parameter indicates which GeoProperty to use for the toplevel geometry field
+ val geometryProperty: String? = null
)
enum class AttributeRepresentation {
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 1d553164fd..09612a8dd2 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
@@ -98,7 +98,7 @@ fun missingPathErrorResponse(errorMessage: String): ResponseEntity<*> {
}
fun buildQueryResponse(
- entities: List,
+ entities: Any,
count: Int,
resourceUrl: String,
paginationQuery: PaginationQuery,
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 f895afa952..cb58f2e832 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
@@ -8,6 +8,7 @@ import arrow.core.right
import arrow.fx.coroutines.parMap
import com.egm.stellio.shared.model.*
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_CONTEXT
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_TERM
import com.egm.stellio.shared.util.JsonLdUtils.extractContextFromInput
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import kotlinx.coroutines.reactive.awaitFirst
@@ -34,6 +35,7 @@ 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_OPTIONS: String = "options"
const val QUERY_PARAM_OPTIONS_SYSATTRS_VALUE: String = "sysAttrs"
const val QUERY_PARAM_OPTIONS_KEYVALUES_VALUE: String = "keyValues"
@@ -171,17 +173,24 @@ fun parseAndExpandRequestParameter(requestParam: String?, contextLink: String):
}.toSet()
fun parseRepresentations(
- optionsParam: List,
+ 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 entityRepresentation = EntityRepresentation.forMediaType(acceptMediaType)
+ val geometryProperty =
+ if (entityRepresentation == EntityRepresentation.GEO_JSON)
+ requestParams.getFirst(QUERY_PARAM_GEOMETRY_PROPERTY) ?: NGSILD_LOCATION_TERM
+ else null
return NgsiLdDataRepresentation(
- EntityRepresentation.forMediaType(acceptMediaType),
+ entityRepresentation,
attributeRepresentation,
- includeSysAttrs
+ includeSysAttrs,
+ geometryProperty
)
}
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 2d28144832..fa917c7029 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
@@ -14,6 +14,12 @@ import org.locationtech.jts.io.WKTWriter
import org.locationtech.jts.io.geojson.GeoJsonReader
import org.locationtech.jts.io.geojson.GeoJsonWriter
+const val FEATURE_TYPE = "Feature"
+const val FEATURE_COLLECTION_TYPE = "FeatureCollection"
+const val GEOMETRY_PROPERTY_TERM = "geometry"
+const val PROPERTIES_PROPERTY_TERM = "properties"
+const val FEATURES_PROPERTY_TERM = "features"
+
fun geoJsonToWkt(geometryType: GeoQuery.GeometryType, coordinates: String): Either =
geoJsonToWkt(
"""
diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt
index e636a8d74a..5021a4c276 100644
--- a/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt
+++ b/shared/src/test/kotlin/com/egm/stellio/shared/model/JsonLdEntityTests.kt
@@ -3,6 +3,7 @@ package com.egm.stellio.shared.model
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CREATED_AT_PROPERTY
+import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_NAME_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
@@ -302,4 +303,211 @@ class JsonLdEntityTests {
assertEquals(expectedEntity, simplifiedEntity)
}
+
+ @Test
+ fun `it should return a normalized GeoJSON entity on location attribute`() {
+ val inputEntity = normalizedJson.deserializeAsMap()
+
+ val expectedEntity =
+ """
+ {
+ "id": "urn:ngsi-ld:Vehicle:A4567",
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ },
+ "properties": {
+ "type": "Vehicle",
+ "brandName": {
+ "type": "Property",
+ "value": "Mercedes"
+ },
+ "isParked": {
+ "type": "Relationship",
+ "object": "urn:ngsi-ld:OffStreetParking:Downtown1",
+ "observedAt": "2017-07-29T12:00:04Z",
+ "providedBy": {
+ "type": "Relationship",
+ "object": "urn:ngsi-ld:Person:Bob"
+ }
+ },
+ "location": {
+ "type": "GeoProperty",
+ "value": {
+ "type": "Point",
+ "coordinates": [
+ 24.30623,
+ 60.07966
+ ]
+ }
+ },
+ "@context": [
+ "https://example.org/ngsi-ld/latest/commonTerms.jsonld",
+ "https://example.org/ngsi-ld/latest/vehicle.jsonld",
+ "https://example.org/ngsi-ld/latest/parking.jsonld",
+ "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"
+ ]
+ }
+ }
+ """.trimIndent().deserializeAsMap()
+
+ val actualEntity = inputEntity.toFinalRepresentation(
+ NgsiLdDataRepresentation(
+ EntityRepresentation.GEO_JSON,
+ AttributeRepresentation.NORMALIZED,
+ includeSysAttrs = false,
+ geometryProperty = NGSILD_LOCATION_TERM
+ )
+ )
+
+ assertEquals(expectedEntity, actualEntity)
+ }
+
+ @Test
+ fun `it should return a simplified GeoJSON entity on location attribute`() {
+ val inputEntity = normalizedJson.deserializeAsMap()
+
+ val expectedEntity =
+ """
+ {
+ "id": "urn:ngsi-ld:Vehicle:A4567",
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ },
+ "properties": {
+ "type": "Vehicle",
+ "brandName": "Mercedes",
+ "isParked": "urn:ngsi-ld:OffStreetParking:Downtown1",
+ "location": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ },
+ "@context": [
+ "https://example.org/ngsi-ld/latest/commonTerms.jsonld",
+ "https://example.org/ngsi-ld/latest/vehicle.jsonld",
+ "https://example.org/ngsi-ld/latest/parking.jsonld",
+ "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld"
+ ]
+ }
+ }
+ """.trimIndent().deserializeAsMap()
+
+ val simplifiedEntity = inputEntity.toFinalRepresentation(
+ NgsiLdDataRepresentation(
+ EntityRepresentation.GEO_JSON,
+ AttributeRepresentation.SIMPLIFIED,
+ includeSysAttrs = false,
+ geometryProperty = NGSILD_LOCATION_TERM
+ )
+ )
+
+ assertEquals(expectedEntity, simplifiedEntity)
+ }
+
+ @Test
+ fun `it should return a GeoJSON entity with a null geometry if the GeoProperty does not exist`() {
+ val inputEntity =
+ """
+ {
+ "id": "urn:ngsi-ld:Vehicle:A4567",
+ "type": "Vehicle",
+ "brandName": {
+ "type": "Property",
+ "value": "Mercedes"
+ }
+ }
+ """.trimIndent().deserializeAsMap()
+
+ val expectedEntity =
+ """
+ {
+ "id": "urn:ngsi-ld:Vehicle:A4567",
+ "type": "Feature",
+ "geometry": null,
+ "properties": {
+ "type": "Vehicle",
+ "brandName": "Mercedes"
+ }
+ }
+ """.trimIndent().deserializeAsMap()
+
+ val simplifiedEntity = inputEntity.toFinalRepresentation(
+ NgsiLdDataRepresentation(
+ EntityRepresentation.GEO_JSON,
+ AttributeRepresentation.SIMPLIFIED,
+ includeSysAttrs = false,
+ geometryProperty = NGSILD_LOCATION_TERM
+ )
+ )
+
+ assertEquals(expectedEntity, simplifiedEntity)
+ }
+
+ @Test
+ fun `it should return simplified GeoJSON entities`() {
+ val inputEntity =
+ """
+ {
+ "id": "urn:ngsi-ld:Vehicle:A4567",
+ "type": "Vehicle",
+ "location": {
+ "type": "GeoProperty",
+ "value": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ }
+ }
+ }
+ """.trimIndent().deserializeAsMap()
+
+ val expectedEntities =
+ """
+ {
+ "type": "FeatureCollection",
+ "features": [{
+ "id": "urn:ngsi-ld:Vehicle:A4567",
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ },
+ "properties": {
+ "type": "Vehicle",
+ "location": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ }
+ }
+ }, {
+ "id": "urn:ngsi-ld:Vehicle:A4567",
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ },
+ "properties": {
+ "type": "Vehicle",
+ "location": {
+ "type": "Point",
+ "coordinates": [ 24.30623, 60.07966 ]
+ }
+ }
+ }]
+ }
+ """.trimIndent().deserializeAsMap()
+
+ val actualEntities = listOf(inputEntity, inputEntity).toFinalRepresentation(
+ NgsiLdDataRepresentation(
+ EntityRepresentation.GEO_JSON,
+ AttributeRepresentation.SIMPLIFIED,
+ includeSysAttrs = false,
+ NGSILD_LOCATION_TERM
+ )
+ )
+
+ assertEquals(expectedEntities, actualEntities)
+ }
}