diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala index b0f798d544..f0618aafc9 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala @@ -19,7 +19,7 @@ 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.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{AnnotatedSource, HttpResponseFields, RdfMarshalling} +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{HttpResponseFields, OriginalSource, RdfMarshalling} import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment, IdSegmentRef, ResourceF} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resolvers.{read => Read, write => Write} @@ -27,7 +27,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResolverRejection.Resol import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{MultiResolutionResult, Resolver, ResolverRejection} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.{MultiResolution, Resolvers} import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef -import io.circe.{Json, Printer} +import io.circe.Json /** * The resolver routes @@ -74,10 +74,13 @@ final class ResolversRoutes( private def emitMetadataOrReject(io: IO[ResolverResource]): Route = emit(io.map(_.void).attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound]) - private def emitSource(io: IO[ResolverResource]): Route = { - implicit val source: Printer = sourcePrinter - emit(io.map(_.value.source).attemptNarrow[ResolverRejection].rejectOn[ResolverNotFound]) - } + private def emitSource(io: IO[ResolverResource]): Route = + emit( + io + .map { resource => OriginalSource(resource, resource.value.source) } + .attemptNarrow[ResolverRejection] + .rejectOn[ResolverNotFound] + ) def routes: Route = (baseUriPrefix(baseUri.prefix) & replaceUri("resolvers", schemas.resolvers)) { @@ -179,7 +182,7 @@ final class ResolversRoutes( case ResolvedResourceOutputType.Source => emit(io.map(_.value.source).attemptNarrow[ResolverRejection]) case ResolvedResourceOutputType.AnnotatedSource => - val annotatedSourceIO = io.map { r => AnnotatedSource(r.value.resource, r.value.source) } + val annotatedSourceIO = io.map { r => OriginalSource.annotated(r.value.resource, r.value.source) } emit(annotatedSourceIO.attemptNarrow[ResolverRejection]) } } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala index 8658ba1337..c97ba2add7 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala @@ -16,7 +16,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ 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.{AnnotatedSource, RdfMarshalling} +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, 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} @@ -24,7 +24,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection._ 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} /** * The resource routes @@ -208,13 +207,10 @@ final class ResourcesRoutes( resourceRef => authorizeFor(project, Read).apply { annotateSource { annotate => - implicit val source: Printer = sourcePrinter emit( resources .fetch(resourceRef, project, schemaOpt) - .map { resource => - AnnotatedSource.when(annotate)(resource, resource.value.source) - } + .map { resource => OriginalSource(resource, resource.value.source, annotate) } .attemptNarrow[ResourceRejection] .rejectOn[ResourceNotFound] ) @@ -309,9 +305,4 @@ object ResourcesRoutes { decodingOption: DecodingOption ): Route = new ResourcesRoutes(identities, aclCheck, resources, index).routes - def asSourceWithMetadata( - resource: ResourceF[Resource] - )(implicit baseUri: BaseUri): Json = - AnnotatedSource(resource, resource.value.source) - } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutes.scala index 04732063e1..788f55fc89 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutes.scala @@ -12,20 +12,20 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering import ch.epfl.bluebrain.nexus.delta.sdk._ import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck -import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaSchemeDirectives} 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.{AnnotatedSource, RdfMarshalling} +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, 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.schemas.{read => Read, write => Write} import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.SchemaRejection.SchemaNotFound import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.{Schema, SchemaRejection} -import io.circe.{Json, Printer} +import io.circe.Json /** * The schemas routes @@ -73,9 +73,8 @@ final class SchemasRoutes( emit(io.map(_.void).attemptNarrow[SchemaRejection].rejectOn[SchemaNotFound]) private def emitSource(io: IO[SchemaResource], annotate: Boolean): Route = { - implicit val source: Printer = sourcePrinter emit( - io.map { resource => AnnotatedSource.when(annotate)(resource, resource.value.source) } + io.map { resource => OriginalSource(resource, resource.value.source, annotate) } .attemptNarrow[SchemaRejection] .rejectOn[SchemaNotFound] ) diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala index 0353b6ffac..208ea1f994 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala @@ -496,6 +496,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with ValidateResourceFixture wit Get(s"/v1/resources/myorg/myproject/_/$id/source") ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual simplePayload(id) + response.expectConditionalCacheHeaders } } } @@ -601,6 +602,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with ValidateResourceFixture wit Get(endpoint) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payloadWithMetadata(id) + response.expectConditionalCacheHeaders response.headers should contain(varyHeader) } } diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala index 0ed7191d6a..e4d817d1e0 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/SchemasRoutesSpec.scala @@ -318,6 +318,7 @@ class SchemasRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues { "id" -> "nxv:myid2", "self" -> ResourceUris.schema(projectRef, myId2).accessUri ) + response.expectConditionalCacheHeaders } } } @@ -336,6 +337,7 @@ class SchemasRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues { Get(endpoint) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK response.asJson shouldEqual payloadNoId + response.expectConditionalCacheHeaders } } } diff --git a/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala b/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala index 30f8c33f2c..3414e1259b 100644 --- a/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala +++ b/delta/plugins/archive/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/archive/ArchiveDownload.scala @@ -27,13 +27,14 @@ import ch.epfl.bluebrain.nexus.delta.sdk.error.SDKError import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.AuthorizationFailed 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.marshalling.OriginalSource import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation._ 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.{AkkaSource, JsonLdValue, ResourceShifts} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import io.circe.{Json, Printer} +import io.circe.syntax.EncoderOps import java.nio.ByteBuffer import java.nio.charset.StandardCharsets @@ -243,8 +244,8 @@ object ArchiveDownload { repr match { case SourceJson => IO.pure(ByteString(prettyPrintSource(value.source))) case AnnotatedSourceJson => - val annotatedSource = AnnotatedSource(value.resource, value.source) - IO.pure(ByteString(prettyPrintSource(annotatedSource))) + val originalSource = OriginalSource.annotated(value.resource, value.source) + IO.pure(ByteString(prettyPrintSource(originalSource.asJson))) 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)) diff --git a/delta/plugins/blazegraph/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/blazegraph/routes/BlazegraphViewsRoutes.scala b/delta/plugins/blazegraph/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/blazegraph/routes/BlazegraphViewsRoutes.scala index e7fabe9872..c756ed0cfa 100644 --- a/delta/plugins/blazegraph/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/blazegraph/routes/BlazegraphViewsRoutes.scala +++ b/delta/plugins/blazegraph/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/blazegraph/routes/BlazegraphViewsRoutes.scala @@ -23,12 +23,12 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdRejection.{DecodingFailed, InvalidJsonLdFormat} -import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, RdfMarshalling} import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._ import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults} import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment} import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef -import io.circe.{Json, Printer} +import io.circe.Json /** * The Blazegraph views routes @@ -83,10 +83,12 @@ class BlazegraphViewsRoutes( private def emitFetch(io: IO[ViewResource]): Route = emit(io.attemptNarrow[BlazegraphViewRejection].rejectOn[ViewNotFound]) - private def emitSource(io: IO[ViewResource]): Route = { - implicit val source: Printer = sourcePrinter - emit(io.map(_.value.source).attemptNarrow[BlazegraphViewRejection].rejectOn[ViewNotFound]) - } + private def emitSource(io: IO[ViewResource]): Route = + emit( + io.map { resource => OriginalSource(resource, resource.value.source) } + .attemptNarrow[BlazegraphViewRejection] + .rejectOn[ViewNotFound] + ) def routes: Route = concat( diff --git a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala index b0382f0692..b6466b2ef0 100644 --- a/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala +++ b/delta/plugins/elasticsearch/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/routes/ElasticSearchViewsRoutes.scala @@ -21,10 +21,10 @@ 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.jsonld.JsonLdRejection.{DecodingFailed, InvalidJsonLdFormat} -import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{OriginalSource, RdfMarshalling} import ch.epfl.bluebrain.nexus.delta.sdk.model._ import io.circe.syntax.EncoderOps -import io.circe.{Json, JsonObject, Printer} +import io.circe.{Json, JsonObject} import java.util.concurrent.TimeUnit import scala.concurrent.duration.Duration @@ -82,10 +82,12 @@ final class ElasticSearchViewsRoutes( private def emitFetch(io: IO[ViewResource]): Route = emit(io.attemptNarrow[ElasticSearchViewRejection].rejectOn[ViewNotFound]) - private def emitSource(io: IO[ViewResource]): Route = { - implicit val source: Printer = sourcePrinter - emit(io.map(_.value.source).attemptNarrow[ElasticSearchViewRejection].rejectOn[ViewNotFound]) - } + private def emitSource(io: IO[ViewResource]): Route = + emit( + io.map { resource => OriginalSource(resource, resource.value.source) } + .attemptNarrow[ElasticSearchViewRejection] + .rejectOn[ViewNotFound] + ) def routes: Route = pathPrefix("views") { diff --git a/delta/plugins/jira/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/jira/routes/JiraRoutes.scala b/delta/plugins/jira/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/jira/routes/JiraRoutes.scala index 7b284eeb27..d1321ff5b9 100644 --- a/delta/plugins/jira/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/jira/routes/JiraRoutes.scala +++ b/delta/plugins/jira/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/jira/routes/JiraRoutes.scala @@ -9,9 +9,9 @@ import ch.epfl.bluebrain.nexus.delta.plugins.jira.{JiraClient, JiraError} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck -import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling import ch.epfl.bluebrain.nexus.delta.sdk.directives.AuthDirectives +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.error.ServiceError.AuthorizationFailed import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala index 90fccd3b6a..0c5b5013da 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/DelegateFilesRoutes.scala @@ -6,7 +6,7 @@ import cats.effect.IO import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileDelegationRequest.{FileDelegationCreationRequest, FileDelegationUpdateRequest} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._ -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileLinkRequest, _} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FileUriDirectives._ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{FileResource, Files} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.ShowFileLocation diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutes.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutes.scala index 52838af3f2..3220dd8987 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutes.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/storages/routes/StoragesRoutes.scala @@ -17,7 +17,7 @@ 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.{OriginalSource, RdfMarshalling} import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri import io.circe.Json import kamon.instrumentation.akka.http.TracingDirectives.operationName @@ -153,13 +153,11 @@ final class StoragesRoutes( }, // Fetch a storage original source (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id)) { id => - operationName(s"$prefixSegment/storages/{org}/{project}/{id}/source") { - authorizeFor(project, Read).apply { - val sourceIO = storages - .fetch(id, project) - .map(res => res.value.source) - emit(sourceIO.attemptNarrow[StorageRejection].rejectOn[StorageNotFound]) - } + authorizeFor(project, Read).apply { + val sourceIO = storages + .fetch(id, project) + .map { resource => OriginalSource(resource, resource.value.source) } + emit(sourceIO.attemptNarrow[StorageRejection].rejectOn[StorageNotFound]) } }, (pathPrefix("statistics") & get & pathEndOrSingleSlash) { diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala index daea262a12..b6b0c826ca 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/DeltaDirectives.scala @@ -54,6 +54,11 @@ trait DeltaDirectives extends UriDirectives { def emit(status: StatusCode, response: ResponseToMarshaller): Route = response(Some(status)) + /** + * Completes the current Route with the provided conversion to original payloads + */ + def emit(response: ResponseToOriginalSource): Route = response() + /** * Completes the current Route with the provided conversion to SSEs */ diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToMarshaller.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToMarshaller.scala index 0fc80a589a..425f1286ea 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToMarshaller.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToMarshaller.scala @@ -22,8 +22,7 @@ trait ResponseToMarshaller { object ResponseToMarshaller extends RdfMarshalling { - // Some resources may not have been created in the system with a strict configuration - // (and if they are, there is no need to check them again) + // To serialize errors to compacted json-ld implicit val api: JsonLdApi = JsonLdJavaApi.lenient private[directives] def apply[E: JsonLdEncoder, A: ToEntityMarshaller]( diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToOriginalSource.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToOriginalSource.scala new file mode 100644 index 0000000000..4d55990144 --- /dev/null +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToOriginalSource.scala @@ -0,0 +1,73 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.directives + +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.model.MediaTypes +import akka.http.scaladsl.server.Directives.{complete, onSuccess, reject} +import akka.http.scaladsl.server.Route +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import cats.syntax.all._ +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.rdf.utils.JsonKeyOrdering +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives.{conditionalCache, requestEncoding} +import ch.epfl.bluebrain.nexus.delta.sdk.directives.Response.{Complete, Reject} +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{HttpResponseFields, OriginalSource, RdfMarshalling} +import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ +import io.circe.syntax.EncoderOps + +/** + * Handles serialization of [[OriginalSource]] and generates the appropriate response headers + */ +trait ResponseToOriginalSource { + def apply(): Route +} + +object ResponseToOriginalSource extends RdfMarshalling { + + // To serialize errors to compacted json-ld + implicit private val api: JsonLdApi = JsonLdJavaApi.lenient + + implicit private def originalSourceMarshaller(implicit + ordering: JsonKeyOrdering + ): ToEntityMarshaller[OriginalSource] = + jsonMarshaller(ordering, sourcePrinter).compose(_.asJson) + + private[directives] def apply[E: JsonLdEncoder]( + io: IO[Either[Response[E], Complete[OriginalSource]]] + )(implicit cr: RemoteContextResolution, jo: JsonKeyOrdering): ResponseToOriginalSource = + () => { + val ioRoute = io.flatMap { + case Left(r: Reject[E]) => IO.pure(reject(r)) + case Left(e: Complete[E]) => e.value.toCompactedJsonLd.map(r => complete(e.status, e.headers, r.json)) + case Right(v: Complete[OriginalSource]) => + IO.pure { + requestEncoding { encoding => + conditionalCache(v.entityTag, v.lastModified, MediaTypes.`application/json`, encoding) { + complete(v.status, v.headers, v.value) + } + } + } + } + onSuccess(ioRoute.unsafeToFuture())(identity) + } + + implicit def ioOriginalPayloadComplete[E: JsonLdEncoder: HttpResponseFields]( + io: IO[Either[E, OriginalSource]] + )(implicit cr: RemoteContextResolution, jo: JsonKeyOrdering): ResponseToOriginalSource = { + val ioComplete = io.map { + _.bimap(e => Complete(e), originalSource => Complete(originalSource)) + } + ResponseToOriginalSource(ioComplete) + } + + implicit def ioResponseOriginalPayloadComplete[E: JsonLdEncoder]( + io: IO[Either[Response[E], OriginalSource]] + )(implicit cr: RemoteContextResolution, jo: JsonKeyOrdering): ResponseToOriginalSource = { + val ioComplete = io.map { + _.map(originalSource => Complete(originalSource)) + } + ResponseToOriginalSource(ioComplete) + } +} diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSource.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSource.scala deleted file mode 100644 index 661c36a8dc..0000000000 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSource.scala +++ /dev/null @@ -1,27 +0,0 @@ -package ch.epfl.bluebrain.nexus.delta.sdk.marshalling - -import cats.syntax.all._ -import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts -import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF} -import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ -import io.circe.Json -import io.circe.syntax.EncoderOps - -object AnnotatedSource { - - /** - * Merge the source with the metadata when annotation is requested or return only the source otherwise - */ - def when(annotate: Boolean)(resourceF: ResourceF[_], source: Json)(implicit baseUri: BaseUri): Json = - if (annotate) apply(resourceF, source) else source - - /** - * Merges the source with the metadata of [[ResourceF]] - */ - def apply(resourceF: ResourceF[_], source: Json)(implicit baseUri: BaseUri): Json = { - val sourceWithoutMetadata = source.removeMetadataKeys() - val metadataJson = resourceF.void.asJson - metadataJson.deepMerge(sourceWithoutMetadata).addContext(contexts.metadata) - } - -} diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/OriginalSource.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/OriginalSource.scala new file mode 100644 index 0000000000..c78de8ec53 --- /dev/null +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/OriginalSource.scala @@ -0,0 +1,83 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.marshalling + +import cats.syntax.all._ +import akka.http.scaladsl.model.{HttpHeader, StatusCode} +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF} +import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ +import io.circe.syntax.EncoderOps +import io.circe.{Encoder, Json} + +import java.time.Instant + +/** + * Defines an original source (what has been provided by clients during the api call) + * + * - We preserve all the provided values (i.e evene null values are preserved) + */ +sealed trait OriginalSource extends Product with Serializable { + + /** + * The resource representation to annotate data and compute the conditional cache headers + */ + def resourceF: ResourceF[Unit] + + /** + * The original payload + */ + def source: Json +} + +object OriginalSource { + + /** + * Standard original source + * - Only the payload provided by the user will be returned + */ + final private case class Standard(resourceF: ResourceF[Unit], source: Json) extends OriginalSource + + /** + * Annotated original source + * - Injects alongside the original source, the metadata context (ex: audit values, @id, ...) + */ + final private case class Annotated(resourceF: ResourceF[Unit], source: Json)(implicit val baseUri: BaseUri) + extends OriginalSource + + def apply[A](resourceF: ResourceF[A], source: Json, annotated: Boolean)(implicit baseUri: BaseUri): OriginalSource = { + if (annotated) + Annotated(resourceF.void, source) + else + apply(resourceF, source) + } + + def apply[A](resourceF: ResourceF[A], source: Json): OriginalSource = Standard(resourceF.void, source) + + def annotated[A](resourceF: ResourceF[A], source: Json)(implicit baseUri: BaseUri): OriginalSource = + apply(resourceF, source, annotated = true) + + implicit val originalSourceEncoder: Encoder[OriginalSource] = + Encoder.instance { + case standard: Standard => + standard.source + case value: Annotated => + implicit val baseUri: BaseUri = value.baseUri + val sourceWithoutMetadata = value.source.removeMetadataKeys() + val metadataJson = value.resourceF.asJson + metadataJson.deepMerge(sourceWithoutMetadata).addContext(contexts.metadata) + } + + implicit val originalSourceHttpResponseFields: HttpResponseFields[OriginalSource] = { + val resourceFHttpResponseField = ResourceF.resourceFHttpResponseFields[Unit] + new HttpResponseFields[OriginalSource] { + override def statusFrom(value: OriginalSource): StatusCode = + resourceFHttpResponseField.statusFrom(value.resourceF) + override def headersFrom(value: OriginalSource): Seq[HttpHeader] = + resourceFHttpResponseField.headersFrom(value.resourceF) + override def entityTag(value: OriginalSource): Option[String] = + resourceFHttpResponseField.entityTag(value.resourceF) + override def lastModified(value: OriginalSource): Option[Instant] = + resourceFHttpResponseField.lastModified(value.resourceF) + } + } + +} diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/RdfMarshalling.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/RdfMarshalling.scala index 2a2c58191e..fe3ed64ad2 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/RdfMarshalling.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/RdfMarshalling.scala @@ -23,8 +23,8 @@ trait RdfMarshalling { val defaultPrinter: Printer = Printer(dropNullValues = true, indent = "") val sourcePrinter: Printer = Printer(dropNullValues = false, indent = "") - private val ntriplesMediaTypes = List(`application/n-triples`, `text/plain`) - private val jsonMediaTypes = List(`application/json`, `application/ld+json`.toContentType) + private val ntriplesMediaTypes = List(`application/n-triples`, `text/plain`) + val jsonMediaTypes: Seq[ContentType.WithFixedCharset] = List(`application/json`, `application/ld+json`.toContentType) /** * JsonLd -> HttpEntity diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/model/MultiFetchResponse.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/model/MultiFetchResponse.scala index 9765329eb6..b5dc7b9b83 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/model/MultiFetchResponse.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/model/MultiFetchResponse.scala @@ -8,7 +8,7 @@ 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.marshalling.AnnotatedSource +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.OriginalSource 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 @@ -93,7 +93,7 @@ object MultiFetchResponse { val source = content.source repr match { case SourceJson => IO.pure(source.asJson) - case AnnotatedSourceJson => IO.pure(AnnotatedSource(value, source)) + case AnnotatedSourceJson => IO.pure(OriginalSource.annotated(value, 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 } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSourceSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/OriginalSourceSuite.scala similarity index 91% rename from delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSourceSuite.scala rename to delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/OriginalSourceSuite.scala index 5b15726a71..6d3c28355c 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/AnnotatedSourceSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/marshalling/OriginalSourceSuite.scala @@ -15,7 +15,7 @@ import munit.Location import java.time.Instant -class AnnotatedSourceSuite extends NexusSuite with CirceLiteral { +class OriginalSourceSuite extends NexusSuite with CirceLiteral { implicit val baseUri: BaseUri = BaseUri("http://localhost", Label.unsafe("v1")) @@ -52,13 +52,13 @@ class AnnotatedSourceSuite extends NexusSuite with CirceLiteral { }""" private def assertResult( - result: Json, + result: OriginalSource, expectedId: String, expectedType: String, expectedContext: ContextValue, payloadFields: (String, Json)* - )(implicit l: Location) = { - def onObject(obj: JsonObject) = { + )(implicit l: Location): Unit = { + def onObject(obj: JsonObject): Unit = { assertEquals(obj("@id"), Some(expectedId.asJson)) assertEquals(obj("@type"), Some(expectedType.asJson)) assertEquals(obj("@context"), Some(expectedContext.asJson)) @@ -68,7 +68,7 @@ class AnnotatedSourceSuite extends NexusSuite with CirceLiteral { assertEquals(payloadData, JsonObject(payloadFields: _*)) } - result.arrayOrObject( + result.asJson.arrayOrObject( fail("We expected an object, we got a literal"), _ => fail("We expected an object, we got an array"), onObject @@ -78,7 +78,7 @@ class AnnotatedSourceSuite extends NexusSuite with CirceLiteral { test("Merge metadata and source for a resource without an id, type or context") { val source = json"""{"source": "original payload" }""" assertResult( - AnnotatedSource(resource, source), + OriginalSource.annotated(resource, source), id.toString, resource.types.mkString, ContextRemoteIri(contexts.metadata), @@ -89,7 +89,7 @@ class AnnotatedSourceSuite extends NexusSuite with CirceLiteral { test("Exclude invalid metadata at the root level") { val source = json"""{"source": "original payload", "_rev": 42, "_other": "xxx", "nested": { "_rev": 5} }""" assertResult( - AnnotatedSource(resource, source), + OriginalSource.annotated(resource, source), id.toString, resource.types.mkString, ContextRemoteIri(contexts.metadata), @@ -103,7 +103,7 @@ class AnnotatedSourceSuite extends NexusSuite with CirceLiteral { val sourceType = "Type" val source = json"""{ "@id": "$sourceId", "@type": "$sourceType", "source": "original payload" }""" assertResult( - AnnotatedSource(resource, source), + OriginalSource.annotated(resource, source), "id", "Type", ContextRemoteIri(contexts.metadata), @@ -123,7 +123,7 @@ class AnnotatedSourceSuite extends NexusSuite with CirceLiteral { "source": "original payload" }""" assertResult( - AnnotatedSource(resource, source), + OriginalSource.annotated(resource, source), "id", "Type", ContextValue(sourceContext, contexts.metadata), diff --git a/docs/src/main/paradox/docs/delta/api/conditional-requests.md b/docs/src/main/paradox/docs/delta/api/conditional-requests.md new file mode 100644 index 0000000000..d3e5aafa9d --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/conditional-requests.md @@ -0,0 +1,23 @@ +# Conditional requests + +Nexus Delta supports conditional requests as defined @link:[here](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-p4-conditional-26) +for the different operations: +* Fetch operation for the different types of resource +* Fetch original payloads for the different types of resources +* Fetching the file contents + +The response for those operations are augmented with respective `ETag` and `Last-Modified` response headers. + +The client can then use those values to set up caches and save bandwidth by using the +@link[conditional headers](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-p4-conditional-26#section-3) +as Delta can immediately answer with a `304 Not Modified` and not resend the full response. + +@@@ note { .tip title="Content negotiation and encoding" } + +Nexus will return different etags for the same resource depending on the `Accept` header +(more about content negotiation @ref[here](content-negotiation.md)) and the `Accept-Encoding` header. + +@@@ + + + diff --git a/docs/src/main/paradox/docs/delta/api/index.md b/docs/src/main/paradox/docs/delta/api/index.md index 5592d412e8..f459528312 100644 --- a/docs/src/main/paradox/docs/delta/api/index.md +++ b/docs/src/main/paradox/docs/delta/api/index.md @@ -1,6 +1,7 @@ @@@ index * @ref:[Content Negotiation](content-negotiation.md) +* @ref:[Conditional requests](conditional-requests.md) * @ref:[Error Signaling](error-signaling.md) * @ref:[Version](version.md) * @ref:[Authentication & Authorization](authentication.md) diff --git a/docs/src/main/paradox/docs/releases/v1.11-release-notes.md b/docs/src/main/paradox/docs/releases/v1.11-release-notes.md index 0f2f167b92..2dd7ea4a6e 100644 --- a/docs/src/main/paradox/docs/releases/v1.11-release-notes.md +++ b/docs/src/main/paradox/docs/releases/v1.11-release-notes.md @@ -9,6 +9,12 @@ For the detailed list of updates in this release, see the @link:[list of address ## Nexus Delta +## Conditional requests + +Nexus now supports conditional requests + +More @ref[here](../delta/api/conditional-requests.md) + ### Remote storage server The remote storage server part has been removed. diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala index f1cc424bb3..0815eb2eca 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala @@ -206,16 +206,24 @@ class ResourcesSpec extends BaseIntegrationSpec { } } - "return not modified when passing a valid etag" in { - val resourceUrl = s"/resources/$project1/test-schema/test-resource:1" - for { - response <- deltaClient.getResponse(resourceUrl, Morty) - etag = response.header[ETag].value.etag - ifNoneMatch = `If-None-Match`(etag) - _ <- deltaClient.get[ByteString](resourceUrl, Morty, jsonHeaders :+ ifNoneMatch) { (_, response) => - response.status shouldEqual StatusCodes.NotModified - } - } yield succeed + "return not modified when fetching the resource or its original payload passing a valid etag" in { + val urls = List( + s"/resources/$project1/test-schema/test-resource:1", + s"/resources/$project1/test-schema/test-resource:1/source" + ) + + forAll(urls) { url => + eventually { + for { + response <- deltaClient.getResponse(url, Morty) + etag = response.header[ETag].value.etag + ifNoneMatch = `If-None-Match`(etag) + _ <- deltaClient.get[ByteString](url, Morty, jsonHeaders :+ ifNoneMatch) { (_, response) => + response.status shouldEqual StatusCodes.NotModified + } + } yield succeed + } + } } "fetch the original payload" in {