Skip to content

Commit

Permalink
feat: first error handling and fix pr comments
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasBousselin committed Oct 10, 2024
1 parent cc6de35 commit 21561cc
Show file tree
Hide file tree
Showing 11 changed files with 74 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ class ContextSourceRegistrationHandler(
applicationProperties.pagination.limitMax
).bind()
val contextSourceRegistrations = contextSourceRegistrationService.getContextSourceRegistrations(
paginationQuery.limit,
paginationQuery.offset,
sub
limit = paginationQuery.limit,
offset = paginationQuery.offset,
sub = sub,
).serialize(contexts, mediaType, includeSysAttrs)
val contextSourceRegistrationsCount = contextSourceRegistrationService.getContextSourceRegistrationsCount(
sub
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ data class CSRFilters( // todo use a combination of EntitiesQuery TemporalQuery

) {
fun buildWHEREStatement(): String {
val idsMatcher = if (ids.isNotEmpty()) "(" +
"entity_info.id is null" +
" OR entity_info.id in ('${ids.joinToString("', '")}')" +
") AND (" +
"entity_info.\"idPattern\" is null OR " +
ids.map { "'$it' ~ entity_info.\"idPattern\"" }.joinToString(" OR ") +
")"
val idsMatcher = if (ids.isNotEmpty()) """"(
entity_info.id is null
OR entity_info.id in ('${ids.joinToString("', '")}')
) AND (
entity_info.\"idPattern\" is null OR
${ids.joinToString(" OR ") { "'$it' ~ entity_info.\"idPattern\"" }}
)
""".trimIndent()
else "true"
return idsMatcher
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ class ContextSourceRegistrationService(
}

suspend fun getContextSourceRegistrations(
limit: Int,
offset: Int,
sub: Option<Sub>,
filters: CSRFilters = CSRFilters()
filters: CSRFilters = CSRFilters(),
limit: Int = Int.MAX_VALUE,
offset: Int = 0,
): List<ContextSourceRegistration> {
val filterQuery = filters.buildWHEREStatement()

Expand Down Expand Up @@ -186,7 +186,6 @@ class ContextSourceRegistrationService(
LIMIT :limit
OFFSET :offset
""".trimIndent()
println(selectStatement)
return databaseClient.sql(selectStatement)
.bind("limit", limit)
.bind("offset", offset)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.egm.stellio.search.csr.service

import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.egm.stellio.search.csr.model.ContextSourceRegistration
import com.egm.stellio.search.csr.model.Mode
import com.egm.stellio.shared.model.*
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_COMPACTED_ENTITY_CORE_MEMBERS
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_TERM
Expand All @@ -10,6 +15,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.logger
import com.egm.stellio.shared.util.isDate
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBody
import org.springframework.web.reactive.function.client.awaitExchange
Expand All @@ -18,7 +24,7 @@ import kotlin.random.Random.Default.nextBoolean

typealias SingleAttribute = Map<String, Any> // todo maybe use the actual attribute type
typealias CompactedAttribute = List<SingleAttribute>
typealias CompactedEntityWithIsAuxiliary = Pair<CompactedEntity, Boolean>
typealias CompactedEntityWithMode = Pair<CompactedEntity, Mode>

object ContextSourceUtils {

Expand All @@ -28,7 +34,7 @@ object ContextSourceUtils {
method: HttpMethod,
path: String,
body: String? = null
): CompactedEntity? {
): Either<APIException, CompactedEntity> = either {
val uri = "${csr.endpoint}$path"
val request = WebClient.create(uri)
.method(method)
Expand All @@ -38,31 +44,37 @@ object ContextSourceUtils {
.awaitExchange { response -> response.statusCode() to response.awaitBody<CompactedEntity>() }
return if (statusCode.is2xxSuccessful) {
logger.info("Successfully received Informations from CSR at : $uri")

response
response.right()
} else {
logger.info("Error contacting CSR at : $uri")
logger.info("Error contacting CSR at : $response")
null
ContextSourceRequestException(
response.toString(),
HttpStatus.valueOf(statusCode.value())
).left()
}
}

fun mergeEntity(
localEntity: CompactedEntity?,
pairsOfEntitiyWithISAuxiliary: List<CompactedEntityWithIsAuxiliary>
pairsOfEntitiyWithMode: List<CompactedEntityWithMode>
): CompactedEntity? {
if (localEntity == null && pairsOfEntitiyWithISAuxiliary.isEmpty()) return null
if (localEntity == null && pairsOfEntitiyWithMode.isEmpty()) return null

val mergedEntity = localEntity?.toMutableMap() ?: mutableMapOf()

pairsOfEntitiyWithISAuxiliary.forEach {
pairsOfEntitiyWithMode.forEach {
entity ->
entity.first.entries.forEach {
(key, value) ->
when {
!mergedEntity.containsKey(key) -> mergedEntity[key] = value
JSONLD_COMPACTED_ENTITY_CORE_MEMBERS.contains(key) -> {}
else -> mergedEntity[key] = mergeAttribute(mergedEntity[key]!!, value, entity.second)
else -> mergedEntity[key] = mergeAttribute(
mergedEntity[key]!!,
value,
entity.second == Mode.AUXILIARY
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ class EntityQueryService(
else null
val formattedType = entitiesQuery.typeSelection?.let { "(" + buildTypeQuery(it) + ")" }
val formattedAttrs =

if (entitiesQuery.attrs.isNotEmpty())
entitiesQuery.attrs.joinToString(
separator = ",",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.egm.stellio.search.csr.model.CSRFilters
import com.egm.stellio.search.csr.model.Mode
import com.egm.stellio.search.csr.service.CompactedEntityWithIsAuxiliary
import com.egm.stellio.search.csr.service.ContextSourceRegistrationService
import com.egm.stellio.search.csr.service.ContextSourceUtils
import com.egm.stellio.search.entity.service.EntityQueryService
Expand Down Expand Up @@ -204,35 +202,36 @@ class EntityHandler(
val csrFilters = CSRFilters(setOf(entityId))

val matchingCSR = contextSourceRegistrationService.getContextSourceRegistrations(
limit = Int.MAX_VALUE,
offset = 0,
sub,
csrFilters
)

// todo local parameter (6.3.18)
// todo parrallelize calls
val localEntity: CompactedEntity? = either {
val localEntity = either {
val expandedEntity = entityQueryService.queryEntity(entityId, sub.getOrNull()).bind()
expandedEntity.checkContainsAnyOf(queryParams.attrs).bind()

val filteredExpandedEntity = ExpandedEntity(
expandedEntity.filterAttributes(queryParams.attrs, queryParams.datasetId)
)
compactEntity(filteredExpandedEntity, contexts)
}.fold({ null }, { it })
}

val compactedEntitiesWithIsAuxiliary: List<CompactedEntityWithIsAuxiliary> = matchingCSR.mapNotNull { csr ->
val entitiesWithMode = matchingCSR.map { csr ->
ContextSourceUtils.call(
httpHeaders,
csr,
HttpMethod.GET,
"/ngsi-ld/v1/entities/$entityId",
null
)?.let { entity -> entity to (csr.mode == Mode.AUXILIARY) }
}

val mergeEntity = ContextSourceUtils.mergeEntity(localEntity, compactedEntitiesWithIsAuxiliary)
"/ngsi-ld/v1/entities/$entityId"
) to csr.mode
}.filter { it.first.isRight() } // ignore all errors
.map { (response, mode) ->
response.getOrNull()!! to mode
}

if (localEntity.isLeft() && entitiesWithMode.isEmpty()) localEntity.bind()
val mergeEntity = ContextSourceUtils.mergeEntity(localEntity.getOrNull(), entitiesWithMode)
?: throw ResourceNotFoundException("No entity with id: $entityId found")

val ngsiLdDataRepresentation = parseRepresentations(params, mediaType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.egm.stellio.search.authorization.web

import com.egm.stellio.search.authorization.service.AuthorizationService
import com.egm.stellio.search.common.config.SearchProperties
import com.egm.stellio.search.csr.service.ContextSourceRegistrationService
import com.egm.stellio.search.entity.service.EntityEventService
import com.egm.stellio.search.entity.service.EntityQueryService
import com.egm.stellio.search.entity.service.EntityService
Expand Down Expand Up @@ -39,6 +40,9 @@ class AnonymousUserHandlerTests {
@MockkBean
private lateinit var entityQueryService: EntityQueryService

@MockkBean
private lateinit var contextSourceRegistrationService: ContextSourceRegistrationService

@Test
@WithAnonymousUser
fun `it should not authorize an anonymous to call the API`() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.egm.stellio.search.entity.web
import arrow.core.left
import arrow.core.right
import com.egm.stellio.search.common.config.SearchProperties
import com.egm.stellio.search.csr.service.ContextSourceRegistrationService
import com.egm.stellio.search.entity.model.*
import com.egm.stellio.search.entity.service.EntityQueryService
import com.egm.stellio.search.entity.service.EntityService
Expand All @@ -28,6 +29,7 @@ import kotlinx.coroutines.test.runTest
import org.hamcrest.core.Is
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
Expand All @@ -46,6 +48,7 @@ import java.time.*
@ActiveProfiles("test")
@WebFluxTest(EntityHandler::class)
@EnableConfigurationProperties(ApplicationProperties::class, SearchProperties::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class EntityHandlerTests {

@Autowired
Expand All @@ -60,8 +63,16 @@ class EntityHandlerTests {
@MockkBean
private lateinit var entityQueryService: EntityQueryService

@MockkBean
private lateinit var contextSourceRegistrationService: ContextSourceRegistrationService

@BeforeAll
fun configureWebClientDefaults() {
coEvery {
contextSourceRegistrationService
.getContextSourceRegistrations(any(), any(), any(), any())
} returnsMany listOf() andThen listOf()

webClient = webClient.mutate()
.apply(mockJwt().jwt { it.subject(MOCK_USER_SUB) })
.apply(csrf())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.egm.stellio.shared.model

import com.apicatalog.jsonld.JsonLdError
import com.apicatalog.jsonld.JsonLdErrorCode
import org.springframework.http.HttpStatus

sealed class APIException(
override val message: String
Expand All @@ -20,6 +21,7 @@ data class NotImplementedException(override val message: String) : APIException(
data class LdContextNotAvailableException(override val message: String) : APIException(message)
data class NonexistentTenantException(override val message: String) : APIException(message)
data class NotAcceptableException(override val message: String) : APIException(message)
data class ContextSourceRequestException(override val message: String, val status: HttpStatus) : APIException(message)

fun Throwable.toAPIException(specificMessage: String? = null): APIException =
when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ data class NonexistentTenantResponse(override val detail: String) :
detail
)

data class ContextSourceRequestResponse(override val detail: String) :
ErrorResponse(
ErrorType.NONEXISTENT_TENANT.type,
"The context source call failed",
detail
)

enum class ErrorType(val type: URI) {
INVALID_REQUEST(URI("https://uri.etsi.org/ngsi-ld/errors/InvalidRequest")),
BAD_REQUEST_DATA(URI("https://uri.etsi.org/ngsi-ld/errors/BadRequestData")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private val logger = LoggerFactory.getLogger("com.egm.stellio.shared.util.ApiRes
* this is globally duplicating what is in ExceptionHandler#transformErrorResponse()
* but main code there should move here when we no longer raise business exceptions
*/
// todo put in ApiException File?
fun APIException.toErrorResponse(): ResponseEntity<*> =
when (this) {
is AlreadyExistsException ->
Expand All @@ -87,6 +88,8 @@ fun APIException.toErrorResponse(): ResponseEntity<*> =
generateErrorResponse(HttpStatus.SERVICE_UNAVAILABLE, LdContextNotAvailableResponse(this.message))
is TooManyResultsException ->
generateErrorResponse(HttpStatus.FORBIDDEN, TooManyResultsResponse(this.message))
is ContextSourceRequestException ->
generateErrorResponse(this.status, ContextSourceRequestResponse(this.message))
else -> generateErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, InternalErrorResponse("$cause"))
}

Expand Down

0 comments on commit 21561cc

Please sign in to comment.