Skip to content

Commit

Permalink
Add annotated source as a format for multi-fetch and archives (#4246)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Dumas <simon.dumas@epfl.ch>
  • Loading branch information
imsdu and Simon Dumas authored Sep 5, 2023
1 parent e97ab4f commit 299570e
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ class MultiFetchRoutes(
}

private def selectPrinter(request: MultiFetchRequest) =
if (request.format == ResourceRepresentation.SourceJson)
if (
request.format == ResourceRepresentation.SourceJson ||
request.format == ResourceRepresentation.AnnotatedSourceJson
)
sourcePrinter
else
defaultPrinter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.schemas
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
Expand All @@ -18,14 +17,14 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaScheme
import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{AnnotatedSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model.routes.Tag
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources.{read => Read, write => Write}
import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption
import ch.epfl.bluebrain.nexus.delta.sdk.resources.{NexusSource, Resources}
import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{InvalidJsonLdFormat, InvalidSchemaRejection, ResourceNotFound}
import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.{Resource, ResourceRejection}
import ch.epfl.bluebrain.nexus.delta.sdk.resources.{NexusSource, Resources}
import io.circe.{Json, Printer}
import monix.bio.IO
import monix.execution.Scheduler
Expand Down Expand Up @@ -284,30 +283,9 @@ object ResourcesRoutes {
decodingOption: DecodingOption
): Route = new ResourcesRoutes(identities, aclCheck, resources, projectsDirectives, index).routes

implicit private val api: JsonLdApi = JsonLdJavaApi.lenient

def asSourceWithMetadata(
resource: ResourceF[Resource]
)(implicit baseUri: BaseUri, cr: RemoteContextResolution): IO[ResourceRejection, Json] = {
metadataJson(resource)
.map(mergeOriginalPayloadWithMetadata(resource.value.source, _))
}

private def metadataJson(resource: ResourceF[Resource])(implicit baseUri: BaseUri, cr: RemoteContextResolution) = {
implicit val resourceFJsonLdEncoder: JsonLdEncoder[ResourceF[Unit]] = ResourceF.defaultResourceFAJsonLdEncoder
resourceFJsonLdEncoder
.compact(resource.void)
.map(_.json)
.mapError(e => InvalidJsonLdFormat(Some(resource.id), e))
}

private def mergeOriginalPayloadWithMetadata(payload: Json, metadata: Json): Json = {
getId(payload)
.foldLeft(payload.deepMerge(metadata))(setId)
}

private def getId(payload: Json): Option[String] = payload.hcursor.get[String]("@id").toOption
private def setId(payload: Json, id: String): Json =
payload.hcursor.downField("@id").set(Json.fromString(id)).top.getOrElse(payload)
)(implicit baseUri: BaseUri, cr: RemoteContextResolution): IO[ResourceRejection, Json] =
AnnotatedSource(resource, resource.value.source).mapError(e => InvalidJsonLdFormat(Some(resource.id), e))

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"format": "annotated-source",
"resources": [
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/success",
"project": "org/proj1",
"value": {
"@context": "https://bluebrain.github.io/nexus/contexts/metadata.json",
"@id": "https://bluebrain.github.io/nexus/vocabulary/success",
"@type": "https://bluebrain.github.io/nexus/vocabulary/Custom",
"bool": false,
"name": "Alex",
"number": 24,
"_constrainedBy": "https://bluebrain.github.io/nexus/schemas/unconstrained.json",
"_createdAt": "1970-01-01T00:00:00Z",
"_createdBy": "http://localhost/v1/anonymous",
"_deprecated": false,
"_incoming": "http://localhost/v1/resources/org/proj1/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2Fsuccess/incoming",
"_outgoing": "http://localhost/v1/resources/org/proj1/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2Fsuccess/outgoing",
"_project": "http://localhost/v1/projects/org/proj1",
"_rev": 1,
"_schemaProject": "http://localhost/v1/projects/org/proj1",
"_self": "http://localhost/v1/resources/org/proj1/_/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2Fsuccess",
"_updatedAt": "1970-01-01T00:00:00Z",
"_updatedBy": "http://localhost/v1/anonymous"
}
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/not-found",
"error": {
"@type": "NotFound",
"reason": "The resource 'https://bluebrain.github.io/nexus/vocabulary/not-found' was not found in project 'org/proj1'."
},
"project": "org/proj1"
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/unauthorized",
"error": {
"@type": "AuthorizationFailed",
"reason": "The supplied authentication is not authorized to access this resource."
},
"project": "org/proj2"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ class MultiFetchRoutesSpec extends BaseRouteSpec {
}
}

"return expected results as annotated source for a user with limited access" in {
val entity = request(ResourceRepresentation.AnnotatedSourceJson).toEntity
Get(endpoint, entity) ~> asAlice ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual jsonContentOf("multi-fetch/annotated-source-response.json")
}
}

"return expected results as original payloads for a user with limited access" in {
val entity = request(ResourceRepresentation.SourceJson).toEntity
Get(endpoint, entity) ~> asAlice ~> routes ~> check {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.Response.Complete
import ch.epfl.bluebrain.nexus.delta.sdk.error.SDKError
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.AnnotatedSource
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceRepresentation}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources
import ch.epfl.bluebrain.nexus.delta.sdk.stream.StreamConverter
Expand Down Expand Up @@ -253,12 +254,16 @@ object ArchiveDownload {
): IO[RdfError, ByteString] = {
implicit val encoder: JsonLdEncoder[A] = value.encoder
repr match {
case SourceJson => UIO.pure(ByteString(prettyPrintSource(value.source)))
case CompactedJsonLd => value.resource.toCompactedJsonLd.map(v => ByteString(prettyPrint(v.json)))
case ExpandedJsonLd => value.resource.toExpandedJsonLd.map(v => ByteString(prettyPrint(v.json)))
case NTriples => value.resource.toNTriples.map(v => ByteString(v.value))
case NQuads => value.resource.toNQuads.map(v => ByteString(v.value))
case Dot => value.resource.toDot.map(v => ByteString(v.value))
case SourceJson => UIO.pure(ByteString(prettyPrintSource(value.source)))
case AnnotatedSourceJson =>
AnnotatedSource(value.resource, value.source).map { json =>
ByteString(prettyPrintSource(json))
}
case CompactedJsonLd => value.resource.toCompactedJsonLd.map(v => ByteString(prettyPrint(v.json)))
case ExpandedJsonLd => value.resource.toExpandedJsonLd.map(v => ByteString(prettyPrint(v.json)))
case NTriples => value.resource.toNTriples.map(v => ByteString(v.value))
case NQuads => value.resource.toNQuads.map(v => ByteString(v.value))
case Dot => value.resource.toDot.map(v => ByteString(v.value))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.data.NonEmptySet
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveReference.{FileReference, ResourceReference}
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveRejection.{DecodingFailed, InvalidJsonLdFormat, UnexpectedArchiveId}
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation.{CompactedJsonLd, Dot, ExpandedJsonLd, NTriples, SourceJson}
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation.{AnnotatedSourceJson, CompactedJsonLd, Dot, ExpandedJsonLd, NTriples, SourceJson}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.AbsolutePath
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv
import ch.epfl.bluebrain.nexus.delta.rdf.implicits._
Expand Down Expand Up @@ -140,11 +140,12 @@ class ArchivesDecodingSpec

"having a resource reference with specific format" in {
val map = Map(
"compacted" -> CompactedJsonLd,
"expanded" -> ExpandedJsonLd,
"n-triples" -> NTriples,
"dot" -> Dot,
"source" -> SourceJson
"compacted" -> CompactedJsonLd,
"expanded" -> ExpandedJsonLd,
"n-triples" -> NTriples,
"dot" -> Dot,
"source" -> SourceJson,
"annotated-source" -> AnnotatedSourceJson
)
forAll(map.toList) { case (format, expFormat) =>
val resourceId = iri"http://localhost/${genString()}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ch.epfl.bluebrain.nexus.delta.sdk.marshalling

import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.rdf.RdfError
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import io.circe.Json
import monix.bio.IO

object AnnotatedSource {

implicit private val api: JsonLdApi = JsonLdJavaApi.lenient

/**
* Merges the source with the metadata of [[ResourceF]]
*/
def apply(resourceF: ResourceF[_], source: Json)(implicit
baseUri: BaseUri,
cr: RemoteContextResolution
): IO[RdfError, Json] =
metadataJson(resourceF)
.map(mergeOriginalPayloadWithMetadata(source, _))

private def metadataJson(resource: ResourceF[_])(implicit baseUri: BaseUri, cr: RemoteContextResolution) = {
implicit val resourceFJsonLdEncoder: JsonLdEncoder[ResourceF[Unit]] = ResourceF.defaultResourceFAJsonLdEncoder
resourceFJsonLdEncoder
.compact(resource.void)
.map(_.json)
}

private def mergeOriginalPayloadWithMetadata(payload: Json, metadata: Json): Json = {
getId(payload)
.foldLeft(payload.deepMerge(metadata))(setId)
}

private def getId(payload: Json): Option[String] = payload.hcursor.get[String]("@id").toOption

private def setId(payload: Json, id: String): Json =
payload.hcursor.downField("@id").set(Json.fromString(id)).top.getOrElse(payload)

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ object ResourceRepresentation {
override val toString: String = "source"
}

/**
* Source representation of a resource.
*/
final case object AnnotatedSourceJson extends ResourceRepresentation {
override def extension: String = ".json"

override val toString: String = "annotated-source"
}

/**
* Compacted JsonLD representation of a resource.
*/
Expand Down Expand Up @@ -73,13 +82,14 @@ object ResourceRepresentation {

private def parse(value: String) =
value match {
case SourceJson.toString => Right(SourceJson)
case CompactedJsonLd.toString => Right(CompactedJsonLd)
case ExpandedJsonLd.toString => Right(ExpandedJsonLd)
case NTriples.toString => Right(NTriples)
case NQuads.toString => Right(NQuads)
case Dot.toString => Right(Dot)
case other => Left(s"$other is not a valid representation")
case SourceJson.toString => Right(SourceJson)
case AnnotatedSourceJson.toString => Right(AnnotatedSourceJson)
case CompactedJsonLd.toString => Right(CompactedJsonLd)
case ExpandedJsonLd.toString => Right(ExpandedJsonLd)
case NTriples.toString => Right(NTriples)
case NQuads.toString => Right(NQuads)
case Dot.toString => Right(Dot)
case other => Left(s"$other is not a valid representation")
}

implicit final val resourceRepresentationJsonLdDecoder: JsonLdDecoder[ResourceRepresentation] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteCon
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.rdf.syntax.jsonLdEncoderSyntax
import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation.{CompactedJsonLd, Dot, ExpandedJsonLd, NQuads, NTriples, SourceJson}
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.AnnotatedSource
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation.{AnnotatedSourceJson, CompactedJsonLd, Dot, ExpandedJsonLd, NQuads, NTriples, SourceJson}
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceRepresentation}
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.model.MultiFetchResponse.Result
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.model.MultiFetchResponse.Result.itemEncoder
Expand Down Expand Up @@ -87,26 +88,33 @@ object MultiFetchResponse {
"project" -> item.project.asJson
)

def valueToJson[A](value: JsonLdContent[A, _]): IO[RdfError, Json] = {
implicit val encoder: JsonLdEncoder[A] = value.encoder
toJson(value.resource, value.source)
def valueToJson[A](content: JsonLdContent[A, _]): IO[RdfError, Json] = {
implicit val encoder: JsonLdEncoder[A] = content.encoder
val value = content.resource
val source = content.source
repr match {
case SourceJson => UIO.pure(source.asJson)
case AnnotatedSourceJson => AnnotatedSource(value, source)
case CompactedJsonLd => value.toCompactedJsonLd.map { v => v.json }
case ExpandedJsonLd => value.toExpandedJsonLd.map { v => v.json }
case NTriples => value.toNTriples.map { v => v.value.asJson }
case NQuads => value.toNQuads.map { v => v.value.asJson }
case Dot => value.toDot.map { v => v.value.asJson }
}
}

def toJson[C, S](value: C, source: S)(implicit
valueJsonLdEncoder: JsonLdEncoder[C],
sourceEncoder: Encoder[S]
): IO[RdfError, Json] =
def onError(error: Error): IO[RdfError, Json] =
repr match {
case SourceJson => UIO.pure(source.asJson)
case CompactedJsonLd => value.toCompactedJsonLd.map { v => v.json }
case ExpandedJsonLd => value.toExpandedJsonLd.map { v => v.json }
case NTriples => value.toNTriples.map { v => v.value.asJson }
case NQuads => value.toNQuads.map { v => v.value.asJson }
case Dot => value.toDot.map { v => v.value.asJson }
case SourceJson | AnnotatedSourceJson => UIO.pure(error.asJson)
case CompactedJsonLd => error.toCompactedJsonLd.map { v => v.json }
case ExpandedJsonLd => error.toExpandedJsonLd.map { v => v.json }
case NTriples => error.toNTriples.map { v => v.value.asJson }
case NQuads => error.toNQuads.map { v => v.value.asJson }
case Dot => error.toDot.map { v => v.value.asJson }
}

val result = item match {
case e: Error => toJson(e, e).map { e => JsonObject("error" -> e) }
case e: Error => onError(e).map { e => JsonObject("error" -> e) }
case Success(_, _, content) => valueToJson(content).map { r => JsonObject("value" -> r) }
}

Expand Down
Loading

0 comments on commit 299570e

Please sign in to comment.