Skip to content

Commit

Permalink
feat(core): add support for GeoJSON representations
Browse files Browse the repository at this point in the history
  • Loading branch information
bobeal committed Nov 26, 2023
1 parent 52634b3 commit fbef632
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class EntityHandler(
/**
* Implements 6.4.3.2 - Query Entities
*/
@GetMapping(produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE])
@GetMapping(produces = [APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE])
suspend fun getEntities(
@RequestHeader httpHeaders: HttpHeaders,
@RequestParam params: MultiValueMap<String, String>
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ class EntityOperationHandler(
/**
* Implements 6.23.3.1 - Query Entities via POST
*/
@PostMapping("/query", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE])
@PostMapping("/query", produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE, GEO_JSON_CONTENT_TYPE])
suspend fun queryEntitiesViaPost(
@RequestHeader httpHeaders: HttpHeaders,
@RequestBody requestBody: Mono<String>,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion shared/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>CyclomaticComplexMethod:ExceptionHandler.kt$ExceptionHandler$@ExceptionHandler fun transformErrorResponse(throwable: Throwable): ResponseEntity&lt;ProblemDetail&gt;</ID>
<ID>LongMethod:JsonLdEntityTests.kt$JsonLdEntityTests$@Test fun `it should return simplified GeoJSON entities`()</ID>
<ID>LongMethod:QueryUtils.kt$private fun transformQQueryToSqlJsonPath( mainAttributePath: List&lt;ExpandedTerm&gt;, trailingAttributePath: List&lt;ExpandedTerm&gt;, operator: String, value: String )</ID>
<ID>LongParameterList:ApiResponses.kt$( body: String, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap&lt;String, String&gt;, mediaType: MediaType, contextLink: String )</ID>
<ID>LongParameterList:ApiResponses.kt$( entities: List&lt;CompactedJsonLdEntity&gt;, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap&lt;String, String&gt;, mediaType: MediaType, contextLink: String )</ID>
<ID>LongParameterList:ApiResponses.kt$( entities: Any, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap&lt;String, String&gt;, mediaType: MediaType, contextLink: String )</ID>
<ID>LongParameterList:NgsiLdEntity.kt$NgsiLdEntity$( val id: URI, val types: List&lt;ExpandedTerm&gt;, val scopes: List&lt;String&gt;?, val relationships: List&lt;NgsiLdRelationship&gt;, val properties: List&lt;NgsiLdProperty&gt;, val geoProperties: List&lt;NgsiLdGeoProperty&gt;, val contexts: List&lt;String&gt; )</ID>
<ID>LongParameterList:NgsiLdEntity.kt$NgsiLdGeoPropertyInstance$( val coordinates: WKTCoordinates, createdAt: ZonedDateTime?, modifiedAt: ZonedDateTime?, observedAt: ZonedDateTime?, datasetId: URI?, properties: List&lt;NgsiLdProperty&gt;, relationships: List&lt;NgsiLdRelationship&gt; )</ID>
<ID>LongParameterList:NgsiLdEntity.kt$NgsiLdPropertyInstance$( val value: Any, val unitCode: String?, createdAt: ZonedDateTime?, modifiedAt: ZonedDateTime?, observedAt: ZonedDateTime?, datasetId: URI?, properties: List&lt;NgsiLdProperty&gt;, relationships: List&lt;NgsiLdRelationship&gt; )</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,6 +117,21 @@ private fun simplifyValue(value: Map<String, Any>): Any {
}
}

fun CompactedJsonLdEntity.toGeoJson(geometryProperty: String): Any {
val geometryAttributeContent = this[geometryProperty] as? Map<String, Any>
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<String, Any> =
this.filter {
!JsonLdUtils.NGSILD_SYSATTRS_TERMS.contains(it.key)
Expand All @@ -133,18 +151,30 @@ fun CompactedJsonLdEntity.withoutSysAttrs(): Map<String, Any> =

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<CompactedJsonLdEntity>.toFinalRepresentation(
ngsiLdDataRepresentation: NgsiLdDataRepresentation
): List<CompactedJsonLdEntity> =
): 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fun missingPathErrorResponse(errorMessage: String): ResponseEntity<*> {
}

fun buildQueryResponse(
entities: List<CompactedJsonLdEntity>,
entities: Any,
count: Int,
resourceUrl: String,
paginationQuery: PaginationQuery,
Expand Down
17 changes: 13 additions & 4 deletions shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -171,17 +173,24 @@ fun parseAndExpandRequestParameter(requestParam: String?, contextLink: String):
}.toSet()

fun parseRepresentations(
optionsParam: List<String>,
requestParams: MultiValueMap<String, String>,
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
)
}

Expand Down Expand Up @@ -233,7 +242,7 @@ fun List<MediaType>.getApplicable(): Either<APIException, MediaType> {
return if (mediaType.includes(MediaType.APPLICATION_JSON))
MediaType.APPLICATION_JSON.right()
else
JSON_LD_MEDIA_TYPE.right()
mediaType.right()
}

fun String.parseTimeParameter(errorMsg: String): Either<String, ZonedDateTime> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<APIException, WKTCoordinates> =
geoJsonToWkt(
"""
Expand Down
Loading

0 comments on commit fbef632

Please sign in to comment.