Skip to content

Commit

Permalink
added jsonldContext initialization and validation
Browse files Browse the repository at this point in the history
  • Loading branch information
ranim-n committed Aug 30, 2024
1 parent 4b8bb5e commit 4254e8b
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 7 deletions.
4 changes: 4 additions & 0 deletions shared/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
<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, contexts: List&lt;String&gt; )</ID>
<ID>LongParameterList:ApiResponses.kt$( entities: Any, count: Int, resourceUrl: String, paginationQuery: PaginationQuery, requestParams: MultiValueMap&lt;String, String&gt;, mediaType: MediaType, contexts: List&lt;String&gt; )</ID>
<ID>MaxLineLength:ApiExceptions.kt$if</ID>
<ID>MaximumLineLength:ApiExceptions.kt$ </ID>
<ID>SpreadOperator:EntityEvent.kt$EntityEvent$( *[ JsonSubTypes.Type(value = EntityCreateEvent::class), JsonSubTypes.Type(value = EntityReplaceEvent::class), JsonSubTypes.Type(value = EntityDeleteEvent::class), JsonSubTypes.Type(value = AttributeAppendEvent::class), JsonSubTypes.Type(value = AttributeReplaceEvent::class), JsonSubTypes.Type(value = AttributeUpdateEvent::class), JsonSubTypes.Type(value = AttributeDeleteEvent::class), JsonSubTypes.Type(value = AttributeDeleteAllInstancesEvent::class) ] )</ID>
<ID>SwallowedException:JsonLdUtils.kt$JsonLdUtils$e: Exception</ID>
<ID>SwallowedException:JsonLdUtils.kt$JsonLdUtils$e: JsonLdError</ID>
<ID>TooGenericExceptionCaught:JsonLdUtils.kt$JsonLdUtils$e: Exception</ID>
<ID>TooManyFunctions:JsonLdUtils.kt$JsonLdUtils</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fun Throwable.toAPIException(specificMessage: String? = null): APIException =
when (this) {
is APIException -> this
is JsonLdError ->
if (this.code == JsonLdErrorCode.LOADING_REMOTE_CONTEXT_FAILED)
if (this.code == JsonLdErrorCode.LOADING_REMOTE_CONTEXT_FAILED || this.code == JsonLdErrorCode.LOADING_DOCUMENT_FAILED)
LdContextNotAvailableException(specificMessage ?: "Unable to load remote context (cause was: $this)")
else BadRequestDataException("Unexpected error while parsing payload (cause was: $this)")
else -> BadRequestDataException(specificMessage ?: this.localizedMessage)
Expand Down
15 changes: 15 additions & 0 deletions shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import com.apicatalog.jsonld.JsonLdError
import com.apicatalog.jsonld.JsonLdOptions
import com.apicatalog.jsonld.context.cache.LruCache
import com.apicatalog.jsonld.document.JsonDocument
import com.apicatalog.jsonld.http.DefaultHttpClient
import com.apicatalog.jsonld.loader.DocumentLoaderOptions
import com.apicatalog.jsonld.loader.HttpLoader
import com.egm.stellio.shared.model.*
import com.egm.stellio.shared.util.JsonUtils.deserializeAs
import com.egm.stellio.shared.util.JsonUtils.deserializeAsList
Expand Down Expand Up @@ -121,6 +124,7 @@ object JsonLdUtils {
contextCache = LruCache(CONTEXT_CACHE_CAPACITY)
documentCache = LruCache(DOCUMENT_CACHE_CAPACITY)
}
private val loader = HttpLoader(DefaultHttpClient.defaultInstance())

private fun buildContextDocument(contexts: List<String>): JsonStructure {
val contextsArray = Json.createArrayBuilder()
Expand Down Expand Up @@ -244,6 +248,17 @@ object JsonLdUtils {
}
}

fun checkJsonldContext(context: URI) {
try {
val options = DocumentLoaderOptions()
loader.loadDocument(context, options)
} catch (e: JsonLdError) {
throw e.toAPIException(e.cause?.cause?.message)
} catch (e: Exception) {
throw e.toAPIException(e.cause?.cause?.message)
}
}

private fun transformGeoPropertyToWKT(): (Map.Entry<String, Any>) -> Any = {
if (NGSILD_GEO_PROPERTIES_TERMS.contains(it.key)) {
when (it.value) {
Expand Down
5 changes: 5 additions & 0 deletions subscription-service/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>ArgumentListWrapping:SubscriptionServiceTests.kt$SubscriptionServiceTests$(rawSubscription.deserializeAsMap(), emptyList())</ID>
<ID>CyclomaticComplexMethod:SubscriptionServiceTests.kt$SubscriptionServiceTests$@Test fun `it should load a subscription with all possible members`()</ID>
<ID>Indentation:SubscriptionServiceTests.kt$SubscriptionServiceTests$ </ID>
<ID>LongMethod:EntityEventListenerService.kt$EntityEventListenerService$internal suspend fun dispatchEntityEvent(content: String)</ID>
<ID>LongMethod:SubscriptionService.kt$SubscriptionService$@Transactional suspend fun update( subscriptionId: URI, input: Map&lt;String, Any&gt;, contexts: List&lt;String&gt; ): Either&lt;APIException, Unit&gt;</ID>
<ID>LongParameterList:FixtureUtils.kt$( withQueryAndGeoQuery: Pair&lt;Boolean, Boolean&gt; = Pair(true, true), withEndpointReceiverInfo: Boolean = true, withNotifParams: Pair&lt;FormatType, List&lt;String&gt;&gt; = Pair(FormatType.NORMALIZED, emptyList()), withModifiedAt: Boolean = false, georel: String = "within", geometry: String = "Polygon", coordinates: String = "[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]", timeInterval: Int? = null, contexts: List&lt;String&gt; = listOf(NGSILD_TEST_CORE_CONTEXT) )</ID>
<ID>MaxLineLength:NotificationServiceTests.kt$NotificationServiceTests$fun</ID>
<ID>MaxLineLength:SubscriptionServiceTests.kt$SubscriptionServiceTests$val subscription = ParsingUtils.parseSubscription(rawSubscription.deserializeAsMap(), emptyList()).shouldSucceedAndResult()</ID>
<ID>MaximumLineLength:NotificationServiceTests.kt$NotificationServiceTests$ </ID>
<ID>MaximumLineLength:SubscriptionServiceTests.kt$SubscriptionServiceTests$ </ID>
<ID>NoUnusedImports:SubscriptionService.kt$com.egm.stellio.subscription.service.SubscriptionService.kt</ID>
<ID>TooGenericExceptionCaught:SubscriptionService.kt$SubscriptionService$e: Exception</ID>
<ID>TooManyFunctions:SubscriptionService.kt$SubscriptionService</ID>
</CurrentIssues>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,9 @@ class NotificationService(
AttributeRepresentation.SIMPLIFIED
else AttributeRepresentation.NORMALIZED

val context = it.jsonldContext?.toString()?.let { listOf(it) } ?: it.contexts

val compactedEntity = compactEntity(
ExpandedEntity(filteredEntity),
context
listOf(it.jsonldContext.toString())
).toFinalRepresentation(
NgsiLdDataRepresentation(
entityRepresentation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package com.egm.stellio.subscription.service

import arrow.core.Either
import arrow.core.Option
import arrow.core.computations.ResultEffect.bind
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.apicatalog.jsonld.JsonLdError
import com.egm.stellio.shared.model.*
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SUBSCRIPTION_TERM
import com.egm.stellio.shared.util.JsonLdUtils.checkJsonldContext
import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm
import com.egm.stellio.subscription.config.SubscriptionProperties
import com.egm.stellio.subscription.model.*
Expand Down Expand Up @@ -56,6 +59,7 @@ class SubscriptionService(
checkExpiresAtInTheFuture(subscription).bind()
checkIdPatternIsValid(subscription).bind()
checkNotificationTriggersAreValid(subscription).bind()
checkJsonLdContextIsValid(subscription).bind()
}

private fun checkTypeIsSubscription(subscription: Subscription): Either<APIException, Unit> =
Expand Down Expand Up @@ -134,6 +138,18 @@ class SubscriptionService(
else BadRequestDataException("Unknown notification trigger in ${subscription.notificationTrigger}").left()
}

private suspend fun checkJsonLdContextIsValid(subscription: Subscription): Either<APIException, Unit> {
return try {
val jsonldContext = subscription.jsonldContext
if (jsonldContext != null) {
checkJsonldContext(jsonldContext)
}
Unit.right()
} catch (e: APIException) {
e.left()
}
}

@Transactional
suspend fun create(subscription: Subscription, sub: Option<Sub>): Either<APIException, Unit> = either {
validateNewSubscription(subscription).bind()
Expand All @@ -143,6 +159,8 @@ class SubscriptionService(
parseGeoQueryParameters(subscription.geoQ.toMap(), subscription.contexts).bind()
else null
val endpoint = subscription.notification.endpoint
val jsonldContext =
subscription.jsonldContext ?: subscription.contexts.first()

val insertStatement =
"""
Expand Down Expand Up @@ -182,7 +200,7 @@ class SubscriptionService(
.bind("sys_attrs", subscription.notification.sysAttrs)
.bind("lang", subscription.lang)
.bind("datasetId", subscription.datasetId?.toTypedArray())
.bind("jsonld_context", subscription.jsonldContext)
.bind("jsonld_context", jsonldContext)
.execute().bind()

geoQuery?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,8 @@ class NotificationServiceTests {
)
),
lang = "fr",
contexts = APIC_COMPOUND_CONTEXTS
contexts = APIC_COMPOUND_CONTEXTS,
jsonldContext = APIC_COMPOUND_CONTEXT.toUri()
)

val expandedEntity = expandJsonLdEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package com.egm.stellio.subscription.service
import arrow.core.Some
import com.egm.stellio.shared.model.BadRequestDataException
import com.egm.stellio.shared.model.EntitySelector
import com.egm.stellio.shared.model.LdContextNotAvailableException
import com.egm.stellio.shared.model.NotImplementedException
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_TERM
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_SUBSCRIPTION_TERM
import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import com.egm.stellio.subscription.model.Endpoint
import com.egm.stellio.subscription.model.EndpointInfo
import com.egm.stellio.subscription.model.Notification
Expand Down Expand Up @@ -240,6 +242,62 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer {
}
}

@Test
fun `it should throw a BadRequestData exception when jsonldContext is not a URI`() = runTest {
val rawSubscription =
"""
{
"id": "urn:ngsi-ld:Subscription:1234567890",
"type": "Subscription",
"entities": [
{
"type": "BeeHive"
}
],
"notification": {
"endpoint": {
"uri": "http://localhost:8084"
}
},
"jsonldContext": "unknownContext"
}
""".trimIndent()

val subscription = ParsingUtils.parseSubscription(rawSubscription.deserializeAsMap(), emptyList()).shouldSucceedAndResult()
subscriptionService.validateNewSubscription(subscription)
.shouldFailWith {
it is BadRequestDataException
}
}

@Test
fun `it should throw a LdContextNotAvailable exception when jsonldContext is not available`() = runTest {
val rawSubscription =
"""
{
"id": "urn:ngsi-ld:Subscription:1234567890",
"type": "Subscription",
"entities": [
{
"type": "BeeHive"
}
],
"notification": {
"endpoint": {
"uri": "http://localhost:8084"
}
},
"jsonldContext": "https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-non-existing.jsonld"
}
""".trimIndent()

val subscription = ParsingUtils.parseSubscription(rawSubscription.deserializeAsMap(), emptyList()).shouldSucceedAndResult()
subscriptionService.validateNewSubscription(subscription)
.shouldFailWith {
it is LdContextNotAvailableException
}
}

@Test
fun `it should load a subscription with minimal required info - entities`() = runTest {
val subscription = loadAndDeserializeSubscription("subscription_minimal_entities.json")
Expand Down Expand Up @@ -330,6 +388,29 @@ class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer {
}
}

@Test
fun `it should initialize jsonldContext with subscription @context if jsonldContext is not provided`() = runTest {
val subscription = loadAndDeserializeSubscription("subscription_minimal_entities.json")
subscriptionService.create(subscription, mockUserSub).shouldSucceed()

val persistedSubscription = subscriptionService.getById(subscription.id)
assertThat(persistedSubscription)
.matches {
it.id == "urn:ngsi-ld:Subscription:1".toUri() &&
it.notification.format == FormatType.NORMALIZED &&
it.notification.endpoint.uri == URI("http://localhost:8084") &&
it.notification.endpoint.accept == Endpoint.AcceptType.JSON &&
(
it.entities != null &&
it.entities!!.size == 1 &&
it.entities!!.all { entitySelector -> entitySelector.typeSelection == BEEHIVE_TYPE }
) &&
it.watchedAttributes == null &&
it.isActive
it.jsonldContext == APIC_COMPOUND_CONTEXT.toUri()
}
}

@Test
fun `it should load a subscription with extra info on last notification`() = runTest {
val subscription = gimmeSubscriptionFromMembers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ fun gimmeRawSubscription(
null

val modifiedAtValue = if (withModifiedAt) Instant.now().atZone(ZoneOffset.UTC) else null
val jsonldContext = contexts.first().toUri()
return Subscription(
type = NGSILD_SUBSCRIPTION_TERM,
subscriptionName = "My Subscription",
Expand All @@ -81,6 +82,7 @@ fun gimmeRawSubscription(
receiverInfo = endpointReceiverInfo
)
),
contexts = contexts
contexts = contexts,
jsonldContext = jsonldContext
)
}

0 comments on commit 4254e8b

Please sign in to comment.