From ded6bc359aab8b4ca64da786e771024f79121f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 00:45:56 +0100 Subject: [PATCH 01/15] wip migrate ResourceRoute to tapir --- .../webapi/routing/v2/ResourcesRouteV2.scala | 25 ---------- .../knora/webapi/slice/common/api/ApiV2.scala | 34 +++++++++---- .../slice/common/api/ApiV2Endpoints.scala | 2 + .../slice/common/api/DocsGenerator.scala | 2 + .../resources/api/ResourcesApiModule.scala | 6 ++- .../resources/api/ResourcesApiRoutes.scala | 3 +- .../resources/api/ResourcesEndpoints.scala | 49 +++++++++++++++++++ .../api/ResourcesEndpointsHandler.scala | 27 ++++++++++ .../slice/resources/api/ValuesEndpoints.scala | 4 +- .../api/ValuesEndpointsHandler.scala | 2 +- ...{ValuesRequestModel.scala => Models.scala} | 26 +++++++--- .../api/service/ResourcesRestService.scala | 42 ++++++++++++++++ .../api/service/ValuesRestService.scala | 4 +- 13 files changed, 179 insertions(+), 47 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala rename webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/{ValuesRequestModel.scala => Models.scala} (55%) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 891eb014c9..036de00f4d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -24,7 +24,6 @@ import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.ValuesValidator -import org.knora.webapi.messages.ValuesValidator.arkTimestampToInstant import org.knora.webapi.messages.ValuesValidator.xsdDateTimeStampToInstant import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* @@ -76,7 +75,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( getResourceHistory() ~ getResourceHistoryEvents() ~ getProjectResourceAndValueHistory() ~ - getResources() ~ getResourcesPreview() ~ getResourcesTei() ~ getResourcesGraph() ~ @@ -261,29 +259,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getResources(): Route = path(resourcesBasePath / Segments) { (resIris: Seq[String]) => - get { requestContext => - val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) - val schemaOptionsTask = RouteUtilV2.getSchemaOptions(requestContext) - val params: Map[IRI, IRI] = requestContext.request.uri.query().toMap - val versionDateParser = (s: String) => xsdDateTimeStampToInstant(s).orElse(arkTimestampToInstant(s)) - val requestTask = for { - resourceIris <- getResourceIris(resIris) - versionDate <- getInstantFromParams(params, "version", "version date", versionDateParser) - targetSchema <- targetSchemaTask - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - schemaOptions <- schemaOptionsTask - } yield ResourcesGetRequestV2( - resourceIris, - versionDate = versionDate, - targetSchema = targetSchema, - schemaOptions = schemaOptions, - requestingUser = requestingUser, - ) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext, targetSchemaTask, schemaOptionsTask.map(Some(_))) - } - } - private def getResourceIris(resIris: Seq[IRI]): IO[BadRequestException, Seq[IRI]] = ZIO .fail(BadRequestException(s"List of provided resource Iris exceeds limit of $resultsPerPage")) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala index 476fe30ecd..146acd5f3e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala @@ -5,14 +5,14 @@ package org.knora.webapi.slice.common.api -import sttp.model.HeaderNames +import sttp.model.ContentTypeRange import sttp.model.MediaType import sttp.tapir.Codec import sttp.tapir.CodecFormat import sttp.tapir.DecodeResult import sttp.tapir.EndpointIO import sttp.tapir.EndpointInput -import sttp.tapir.Validator +import sttp.tapir.extractFromRequest import sttp.tapir.header import sttp.tapir.query @@ -67,7 +67,9 @@ object ApiV2 { } object Inputs { - import Codecs.{apiV2SchemaListCodec, jsonLdRenderingListCodec, markupRenderingListCode} + import Codecs.apiV2SchemaListCodec + import Codecs.jsonLdRenderingListCodec + import Codecs.markupRenderingListCode // ApiV2Schema inputs private val apiV2SchemaHeader = header[Option[ApiV2Schema]](Headers.xKnoraAcceptSchemaHeader) @@ -118,15 +120,29 @@ object ApiV2 { } // RdfFormat input - private val rdfFormat: EndpointIO.Header[RdfFormat] = header[Option[MediaType]](HeaderNames.Accept) + private val supportedMediaTypes: List[MediaType] = List( + MediaType("application", "json"), + MediaType("application", "ld+json"), + MediaType("text", "turtle"), + MediaType("application", "trig"), + MediaType("application", "rdf+xml"), + MediaType("application", "n-quads"), + ) + + private val rdfFormat: EndpointInput.ExtractFromRequest[RdfFormat] = extractFromRequest(_.acceptsContentTypes) .description( - s"""The RDF format to be used for the request. Valid values are: ${RdfFormat.values} + s"""With the Accept header the RDF format used for the response can be specified. + |Valid values are: ${RdfFormat.values}. |If not specified or unknown, the fallback RDF format ${RdfFormat.default} will be used.""".stripMargin, ) - .mapDecode(s => DecodeResult.Value(s.flatMap(RdfFormat.from).getOrElse(RdfFormat.default)))(it => - Some(it.mediaType), - ) - .validate(Validator.pass[RdfFormat]) + .mapDecode(s => + DecodeResult.Value( + MediaType + .bestMatch(supportedMediaTypes, s.getOrElse(Seq.empty)) + .flatMap(RdfFormat.from) + .getOrElse(RdfFormat.default), + ), + )(it => Right(Seq(ContentTypeRange.exact(it.mediaType)))) // FormatOptions input val formatOptions: EndpointInput[FormatOptions] = rdfFormat diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala index cbcc3e0c04..8a491c6a24 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala @@ -10,6 +10,7 @@ import zio.ZLayer import org.knora.webapi.slice.lists.api.ListsEndpointsV2 import org.knora.webapi.slice.resourceinfo.api.ResourceInfoEndpoints +import org.knora.webapi.slice.resources.api.ResourcesEndpoints import org.knora.webapi.slice.resources.api.ValuesEndpoints import org.knora.webapi.slice.search.api.SearchEndpoints import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2 @@ -18,6 +19,7 @@ final case class ApiV2Endpoints( private val authenticationEndpoints: AuthenticationEndpointsV2, private val listsEndpointsV2: ListsEndpointsV2, private val resourceInfoEndpoints: ResourceInfoEndpoints, + private val resourcesEndpoints: ResourcesEndpoints, private val searchEndpoints: SearchEndpoints, private val valuesEndpoints: ValuesEndpoints, ) { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala index 01279866da..f4b57badc4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala @@ -40,6 +40,7 @@ import org.knora.webapi.slice.infrastructure.Jwt import org.knora.webapi.slice.infrastructure.api.ManagementEndpoints import org.knora.webapi.slice.lists.api.ListsEndpointsV2 import org.knora.webapi.slice.resourceinfo.api.ResourceInfoEndpoints +import org.knora.webapi.slice.resources.api.ResourcesEndpoints import org.knora.webapi.slice.resources.api.ValuesEndpoints import org.knora.webapi.slice.search.api.SearchEndpoints import org.knora.webapi.slice.security.Authenticator @@ -97,6 +98,7 @@ object DocsGenerator extends ZIOAppDefault { ProjectsLegalInfoEndpoints.layer, ProjectsEndpoints.layer, ResourceInfoEndpoints.layer, + ResourcesEndpoints.layer, SearchEndpoints.layer, ShaclEndpoints.layer, StoreEndpoints.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala index b590591adb..cdc23bfba7 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala @@ -16,13 +16,14 @@ import org.knora.webapi.slice.common.api.BaseEndpoints import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter +import org.knora.webapi.slice.resources.api.service.ResourcesRestService import org.knora.webapi.slice.resources.api.service.ValuesRestService object ResourcesApiModule extends URModule[ ApiComplexV2JsonLdRequestParser & BaseEndpoints & HandlerMapper & KnoraResponseRenderer & ResourcesResponderV2 & TapirToPekkoInterpreter & ValuesResponderV2, - ResourcesApiRoutes & ValuesEndpoints, + ResourcesApiRoutes & ValuesEndpoints & ResourcesEndpoints, ] { self => override def layer: URLayer[self.Dependencies, self.Provided] = @@ -30,6 +31,9 @@ object ResourcesApiModule ValuesEndpointsHandler.layer, ValuesEndpoints.layer, ValuesRestService.layer, + ResourcesEndpoints.layer, + ResourcesEndpointsHandler.layer, + ResourcesRestService.layer, ResourcesApiRoutes.layer, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala index 8ae48e76e9..32000a82b5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala @@ -11,10 +11,11 @@ import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter class ResourcesApiRoutes( valuesEndpoints: ValuesEndpointsHandler, + resourcesEndpoints: ResourcesEndpointsHandler, tapirToPekko: TapirToPekkoInterpreter, ) { - private val handlers = valuesEndpoints.allHandlers + private val handlers = valuesEndpoints.allHandlers ++ resourcesEndpoints.allHandlers val routes: Seq[Route] = handlers.map(tapirToPekko.toRoute(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala new file mode 100644 index 0000000000..c4a79257a0 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -0,0 +1,49 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.resources.api + +import sttp.model.HeaderNames +import sttp.model.MediaType +import sttp.tapir.* +import sttp.tapir.model.UsernamePassword +import sttp.tapir.server.PartialServerEndpoint +import zio.ZLayer + +import scala.concurrent.Future + +import dsp.errors.RequestRejectedException +import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.api.ApiV2 +import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.resources.api.model.VersionDate + +final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { + + private val base = "v2" / "resources" + + private val versionQuery = query[Option[VersionDate]]("version") + .and(query[Option[VersionDate]]("version date")) + .map { + case (Some(v), _) => Some(v) + case (_, Some(v)) => Some(v) + case _ => None + }(d => (d, d)) + + val getResources = baseEndpoints.withUserEndpoint.get + .in(base / paths) + .in(ApiV2.Inputs.formatOptions) + .in(versionQuery) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + + val endpoints: Seq[AnyEndpoint] = Seq( + getResources, + ).map(_.endpoint.tag("V2 Resources")) +} + +object ResourcesEndpoints { + val layer = ZLayer.derive[ResourcesEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala new file mode 100644 index 0000000000..d47696d584 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -0,0 +1,27 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.resources.api +import zio.* + +import org.knora.webapi.slice.common.api.HandlerMapper +import org.knora.webapi.slice.common.api.SecuredEndpointHandler +import org.knora.webapi.slice.resources.api.service.ResourcesRestService + +final class ResourcesEndpointsHandler( + private val resourcesEndpoints: ResourcesEndpoints, + private val resourcesRestService: ResourcesRestService, + private val mapper: HandlerMapper, +) { + + val allHandlers = + Seq( + SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), + ).map(mapper.mapSecuredEndpointHandler(_)) +} + +object ResourcesEndpointsHandler { + val layer = ZLayer.derive[ResourcesEndpointsHandler] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala index 95e3d53880..9ed759c862 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala @@ -13,7 +13,7 @@ import zio.ZLayer import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.common.api.BaseEndpoints import org.knora.webapi.slice.resources.api.model.ValueUuid -import org.knora.webapi.slice.resources.api.model.ValueVersionDate +import org.knora.webapi.slice.resources.api.model.VersionDate final case class ValuesEndpoints(baseEndpoint: BaseEndpoints) { @@ -21,7 +21,7 @@ final case class ValuesEndpoints(baseEndpoint: BaseEndpoints) { private val resourceIri = path[String].name("resourceIri").description("The IRI of a Resource.") private val valueUuid = path[ValueUuid].name("valueUuid").description("The UUID of a Value.") - private val version = query[Option[ValueVersionDate]]("version") + private val version = query[Option[VersionDate]]("version") private val linkToValuesDocumentation = """Find detailed documentation on docs.dasch.swiss.""" diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala index a836684ef8..5daea9b974 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala @@ -12,7 +12,7 @@ import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.resources.api.model.ValueUuid -import org.knora.webapi.slice.resources.api.model.ValueVersionDate +import org.knora.webapi.slice.resources.api.model.VersionDate import org.knora.webapi.slice.resources.api.service.ValuesRestService final class ValuesEndpointsHandler( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/ValuesRequestModel.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala similarity index 55% rename from webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/ValuesRequestModel.scala rename to webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala index 073d6825ea..d6f033f535 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/ValuesRequestModel.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala @@ -5,12 +5,16 @@ package org.knora.webapi.slice.resources.api.model +import sttp.tapir.Codec +import sttp.tapir.CodecFormat.TextPlain +import sttp.tapir.model.Delimited import zio.json.JsonCodec import java.time.Instant import java.util.UUID import scala.util.Try +import dsp.valueobjects.Iri import dsp.valueobjects.UuidUtil import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.slice.admin.api.Codecs.* @@ -27,16 +31,26 @@ object ValueUuid extends WithFrom[String, ValueUuid] { Try(UuidUtil.decode(str)).toEither.left.map(_.getMessage).map(ValueUuid(_)) } -final case class ValueVersionDate private (value: Instant) extends Value[Instant] -object ValueVersionDate extends WithFrom[String, ValueVersionDate] { +final case class VersionDate private (value: Instant) extends Value[Instant] +object VersionDate extends WithFrom[String, VersionDate] { - given JsonCodec[ValueVersionDate] = JsonCodec[String].transformOrFail(from, _.value.toString) - given TapirCodec.StringCodec[ValueVersionDate] = TapirCodec.stringCodec(from, _.value.toString) + given JsonCodec[VersionDate] = JsonCodec[String].transformOrFail(from, _.value.toString) + given TapirCodec.StringCodec[VersionDate] = TapirCodec.stringCodec(from, _.value.toString) - override def from(str: String): Either[String, ValueVersionDate] = + override def from(str: String): Either[String, VersionDate] = ValuesValidator .xsdDateTimeStampToInstant(str) .orElse(ValuesValidator.arkTimestampToInstant(str)) - .map(ValueVersionDate(_)) + .map(VersionDate(_)) .toRight(s"Invalid value version date: $str") } + +final case class ResourceIri(value: String) +object ResourceIri { + + def from(str: String): Either[String, ResourceIri] = + if Iri.isIri(str) then Right(ResourceIri(str)) else Left(s"Invalid IRI: $str") + + given TapirCodec.StringCodec[ResourceIri] = TapirCodec.stringCodec(ResourceIri.from, _.value) + given Codec[String, Delimited[",", ResourceIri], TextPlain] = Codec.delimited +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala new file mode 100644 index 0000000000..5413dd40be --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -0,0 +1,42 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.resources.api.service +import sttp.model.MediaType +import zio.* + +import org.knora.webapi.responders.v2.ResourcesResponderV2 +import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.api.KnoraResponseRenderer +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse +import org.knora.webapi.slice.resources.api.model.VersionDate + +final case class ResourcesRestService(resourcesService: ResourcesResponderV2, renderer: KnoraResponseRenderer) { + + def getResources(user: User)( + resourceIris: List[String], + formatOptions: FormatOptions, + version: Option[VersionDate], + ): Task[(RenderedResponse, MediaType)] = + resourcesService + .getResourcesV2( + resourceIris, + propertyIri = None, + valueUuid = None, + version.map(_.value), + withDeleted = true, + showDeletedValues = false, + formatOptions.schema, + formatOptions.rendering, + user, + ) + .flatMap(renderer.render(_, formatOptions)) + +} + +object ResourcesRestService { + val layer = ZLayer.derive[ResourcesRestService] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ValuesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ValuesRestService.scala index e89a066870..0eec60f033 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ValuesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ValuesRestService.scala @@ -20,7 +20,7 @@ import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse import org.knora.webapi.slice.resources.api.model.ValueUuid -import org.knora.webapi.slice.resources.api.model.ValueVersionDate +import org.knora.webapi.slice.resources.api.model.VersionDate final class ValuesRestService( private val valuesService: ValuesResponderV2, @@ -32,7 +32,7 @@ final class ValuesRestService( def getValue(user: User)( resourceIri: String, valueUuid: ValueUuid, - versionDate: Option[ValueVersionDate], + versionDate: Option[VersionDate], formatOptions: FormatOptions, ): Task[(RenderedResponse, MediaType)] = render( From 4f6b04de52834a72f384ac8d6c0fee5d601bb2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 09:17:04 +0100 Subject: [PATCH 02/15] Migrate GET v2/resources/history/:resourceIri --- .../responders/v2/ResourcesResponderV2.scala | 18 +++++++ .../webapi/routing/v2/ResourcesRouteV2.scala | 47 ------------------- .../resources/api/ResourcesEndpoints.scala | 26 ++++++++++ .../api/ResourcesEndpointsHandler.scala | 1 + .../slice/resources/api/model/Models.scala | 20 +++----- .../api/service/ResourcesRestService.scala | 11 +++++ 6 files changed, 62 insertions(+), 61 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index a314b35e92..b782c75e20 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -7,6 +7,7 @@ package org.knora.webapi.responders.v2 import com.typesafe.scalalogging.LazyLogging import zio.* +import zio.Task import zio.ZIO import java.time.Instant @@ -50,6 +51,7 @@ import org.knora.webapi.slice.admin.domain.service.LegalInfoService import org.knora.webapi.slice.admin.domain.service.ProjectService import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.domain.service.OntologyService +import org.knora.webapi.slice.resources.api.model.VersionDate import org.knora.webapi.slice.resources.repo.service.ResourcesRepo import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -1328,6 +1330,22 @@ final case class ResourcesResponderV2( ) } + def getResourceHistory( + resourceIri: String, + startDate: Option[VersionDate], + endDate: Option[VersionDate], + requestingUser: User, + ): Task[ResourceVersionHistoryResponseV2] = + getResourceHistoryV2( + ResourceVersionHistoryGetRequestV2( + resourceIri, + withDeletedResource = false, + startDate.map(_.value), + endDate.map(_.value), + requestingUser, + ), + ) + /** * Returns the version history of a resource. * diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 036de00f4d..72527e1095 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -12,8 +12,6 @@ import org.apache.pekko.http.scaladsl.server.Route import zio.* import zio.ZIO -import java.time.Instant - import dsp.errors.BadRequestException import dsp.valueobjects.Iri import org.knora.webapi.* @@ -24,7 +22,6 @@ import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.ValuesValidator -import org.knora.webapi.messages.ValuesValidator.xsdDateTimeStampToInstant import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.v2.SearchResponderV2 @@ -72,7 +69,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( createResource() ~ updateResourceMetadata() ~ getResourcesInProject() ~ - getResourceHistory() ~ getResourceHistoryEvents() ~ getProjectResourceAndValueHistory() ~ getResourcesPreview() ~ @@ -195,49 +191,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getResourceHistory(): Route = - path(resourcesBasePath / "history" / Segment) { (resourceIriStr: IRI) => - get { requestContext => - val getResourceIri = Iri - .validateAndEscapeIri(resourceIriStr) - .toZIO - .orElseFail(BadRequestException(s"Invalid resource IRI: $resourceIriStr")) - val params = requestContext.request.uri.query().toMap - val getStartDate = - getInstantFromParams(params, "startDate", "start date", xsdDateTimeStampToInstant) - val getEndDate = - getInstantFromParams(params, "endDate", "end date", xsdDateTimeStampToInstant) - val requestTask = for { - resourceIri <- getResourceIri - startDate <- getStartDate - endDate <- getEndDate - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - } yield ResourceVersionHistoryGetRequestV2( - resourceIri, - withDeletedResource = false, - startDate, - endDate, - requestingUser, - ) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - - private def getInstantFromParams( - params: Map[String, String], - key: String, - name: String, - dateParser: String => Option[Instant], - ): IO[BadRequestException, Option[Instant]] = - params - .get(key) - .map(dateStr => - ZIO - .fromOption(dateParser(dateStr)) - .mapBoth(_ => BadRequestException(s"Invalid $name: $dateStr"), Some(_)), - ) - .getOrElse(ZIO.none) - private def getResourceHistoryEvents(): Route = path(resourcesBasePath / "resourceHistoryEvents" / Segment) { (resourceIri: IRI) => get { requestContext => diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index c4a79257a0..37d857d87e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -18,6 +18,7 @@ import dsp.errors.RequestRejectedException import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.resources.api.model.IriDto import org.knora.webapi.slice.resources.api.model.VersionDate final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { @@ -32,6 +33,30 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { case _ => None }(d => (d, d)) + private val startDateQuery = query[Option[VersionDate]]("startDate") + .and(query[Option[VersionDate]]("start date")) + .map { + case (Some(v), _) => Some(v) + case (_, Some(v)) => Some(v) + case _ => None + }(d => (d, d)) + + private val endDateQuery = query[Option[VersionDate]]("endDate") + .and(query[Option[VersionDate]]("end date")) + .map { + case (Some(v), _) => Some(v) + case (_, Some(v)) => Some(v) + case _ => None + }(d => (d, d)) + + val getResourcesHistory = baseEndpoints.withUserEndpoint.get + .in(base / "history" / path[IriDto].name("resourceIri")) + .in(ApiV2.Inputs.formatOptions) + .in(startDateQuery) + .in(endDateQuery) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val getResources = baseEndpoints.withUserEndpoint.get .in(base / paths) .in(ApiV2.Inputs.formatOptions) @@ -40,6 +65,7 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { .out(header[MediaType](HeaderNames.ContentType)) val endpoints: Seq[AnyEndpoint] = Seq( + getResourcesHistory, getResources, ).map(_.endpoint.tag("V2 Resources")) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index d47696d584..bd5a7615be 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -18,6 +18,7 @@ final class ResourcesEndpointsHandler( val allHandlers = Seq( + SecuredEndpointHandler(resourcesEndpoints.getResourcesHistory, resourcesRestService.getResourceHistory), SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), ).map(mapper.mapSecuredEndpointHandler(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala index d6f033f535..a832ceedc9 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala @@ -5,11 +5,6 @@ package org.knora.webapi.slice.resources.api.model -import sttp.tapir.Codec -import sttp.tapir.CodecFormat.TextPlain -import sttp.tapir.model.Delimited -import zio.json.JsonCodec - import java.time.Instant import java.util.UUID import scala.util.Try @@ -24,7 +19,6 @@ import org.knora.webapi.slice.common.WithFrom final case class ValueUuid private (value: UUID) extends Value[UUID] object ValueUuid extends WithFrom[String, ValueUuid] { - given JsonCodec[ValueUuid] = JsonCodec[String].transformOrFail(from, _.value.toString) given TapirCodec.StringCodec[ValueUuid] = TapirCodec.stringCodec(from, _.value.toString) override def from(str: String): Either[String, ValueUuid] = @@ -34,7 +28,6 @@ object ValueUuid extends WithFrom[String, ValueUuid] { final case class VersionDate private (value: Instant) extends Value[Instant] object VersionDate extends WithFrom[String, VersionDate] { - given JsonCodec[VersionDate] = JsonCodec[String].transformOrFail(from, _.value.toString) given TapirCodec.StringCodec[VersionDate] = TapirCodec.stringCodec(from, _.value.toString) override def from(str: String): Either[String, VersionDate] = @@ -42,15 +35,14 @@ object VersionDate extends WithFrom[String, VersionDate] { .xsdDateTimeStampToInstant(str) .orElse(ValuesValidator.arkTimestampToInstant(str)) .map(VersionDate(_)) - .toRight(s"Invalid value version date: $str") + .toRight(s"Invalid value for instant: $str") } -final case class ResourceIri(value: String) -object ResourceIri { +final case class IriDto(value: String) +object IriDto { - def from(str: String): Either[String, ResourceIri] = - if Iri.isIri(str) then Right(ResourceIri(str)) else Left(s"Invalid IRI: $str") + given TapirCodec.StringCodec[IriDto] = TapirCodec.stringCodec(IriDto.from, _.value) - given TapirCodec.StringCodec[ResourceIri] = TapirCodec.stringCodec(ResourceIri.from, _.value) - given Codec[String, Delimited[",", ResourceIri], TextPlain] = Codec.delimited + def from(str: String): Either[String, IriDto] = + if Iri.isIri(str) then Right(IriDto(str)) else Left(s"Invalid IRI: $str") } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index 5413dd40be..fd4420db02 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -12,10 +12,21 @@ import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse +import org.knora.webapi.slice.resources.api.model.IriDto import org.knora.webapi.slice.resources.api.model.VersionDate final case class ResourcesRestService(resourcesService: ResourcesResponderV2, renderer: KnoraResponseRenderer) { + def getResourceHistory(user: User)( + resourceIri: IriDto, + formatOptions: FormatOptions, + startDate: Option[VersionDate], + endDate: Option[VersionDate], + ): Task[(RenderedResponse, MediaType)] = + resourcesService + .getResourceHistory(resourceIri.value, startDate, endDate, user) + .flatMap(renderer.render(_, formatOptions)) + def getResources(user: User)( resourceIris: List[String], formatOptions: FormatOptions, From 997f5a31127ab6ad7cac533eef39660cf6f5aa7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 11:56:48 +0100 Subject: [PATCH 03/15] Migrate GET v2/resources/resourceHistoryEvents/:resourceIri --- .../ResourcesMessagesV2Spec.scala | 24 ------- .../v2/ResourcesResponderV2Spec.scala | 71 ++++++++----------- .../resourcemessages/ResourceMessagesV2.scala | 20 ------ .../responders/v2/ResourcesResponderV2.scala | 32 ++++----- .../webapi/routing/v2/ResourcesRouteV2.scala | 11 --- .../resources/api/ResourcesEndpoints.scala | 7 ++ .../api/ResourcesEndpointsHandler.scala | 4 ++ .../api/service/ResourcesRestService.scala | 7 ++ 8 files changed, 58 insertions(+), 118 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala b/integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala index 902c1e8056..734c16b6cd 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala @@ -36,28 +36,4 @@ class ResourcesMessagesV2Spec extends CoreSpec { assert(caught.getMessage === "Invalid project IRI: http://rdfh.ch/0001/thing-with-history") } } - - "Get history events of a single resource" should { - "fail if given resource IRI is not valid" in { - val resourceIri = "invalid-resource-IRI" - val caught = intercept[BadRequestException]( - ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = SharedTestDataADM.imagesUser01, - ), - ) - assert(caught.getMessage === s"Invalid resource IRI: $resourceIri") - } - - "fail if given IRI is not a resource IRI" in { - val resourceIri = "http://rdfh.ch/projects/0001" - val caught = intercept[BadRequestException]( - ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = SharedTestDataADM.imagesUser01, - ), - ) - assert(caught.getMessage === s"Given IRI is not a resource IRI: $resourceIri") - } - } } diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index 955bf2db1b..a9a0bbdb97 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -2388,13 +2388,11 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => "return full history of a-thing-picture resource" in { val resourceIri = "http://rdfh.ch/0001/a-thing-picture" - appActor ! ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = anythingUserProfile, + val events = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), + ), ) - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) - val events: Seq[ResourceAndValueHistoryEvent] = response.historyEvents events.size shouldEqual 3 val createResourceEvents = events.filter(historyEvent => historyEvent.eventType == ResourceAndValueEventsUtil.CREATE_RESOURCE_EVENT) @@ -2410,13 +2408,11 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => "return full history of a resource as events" in { val resourceIri = "http://rdfh.ch/0001/thing-with-history" - appActor ! ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = anythingUserProfile, + val events = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), + ), ) - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) - val events: Seq[ResourceAndValueHistoryEvent] = response.historyEvents events.size should be(9) } @@ -2441,13 +2437,11 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => ), ) - appActor ! ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = anythingUserProfile, + val events = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), + ), ) - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) - val events: Seq[ResourceAndValueHistoryEvent] = response.historyEvents events.size should be(10) val updatePermissionEvent: Option[ResourceAndValueHistoryEvent] = events.find(event => event.eventType == ResourceAndValueEventsUtil.UPDATE_VALUE_PERMISSION_EVENT) @@ -2484,13 +2478,11 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => ), ) - appActor ! ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = anythingUserProfile, + val events = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), + ), ) - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) - val events: Seq[ResourceAndValueHistoryEvent] = response.historyEvents events.size should be(11) val createValueEvent: Option[ResourceAndValueHistoryEvent] = events.find(event => @@ -2531,13 +2523,11 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => ), ) - appActor ! ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = anythingUserProfile, + val events = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), + ), ) - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) - val events: Seq[ResourceAndValueHistoryEvent] = response.historyEvents events.size should be(12) val deleteValueEvent: Option[ResourceAndValueHistoryEvent] = events.find(event => @@ -2551,14 +2541,11 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => "return full history of a deleted resource" in { val resourceIri = "http://rdfh.ch/0001/PHbbrEsVR32q5D_ioKt6pA" - appActor ! ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = anythingUserProfile, + val events = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), + ), ) - - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) - val events: Seq[ResourceAndValueHistoryEvent] = response.historyEvents events.size should be(2) val deleteResourceEvent: Option[ResourceAndValueHistoryEvent] = events.find(event => event.eventType == ResourceAndValueEventsUtil.DELETE_RESOURCE_EVENT) @@ -2579,13 +2566,11 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => expectMsgType[UpdateResourceMetadataResponseV2](timeout) - appActor ! ResourceHistoryEventsGetRequestV2( - resourceIri = resourceIri, - requestingUser = anythingUserProfile, + val events = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), + ), ) - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) - val events: Seq[ResourceAndValueHistoryEvent] = response.historyEvents events.size should be(2) val updateMetadataEvent: Option[ResourceAndValueHistoryEvent] = events.find(event => event.eventType == ResourceAndValueEventsUtil.UPDATE_RESOURCE_METADATA_EVENT) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 057f831056..1a88162499 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -9,7 +9,6 @@ import java.time.Instant import java.util.UUID import dsp.errors.* -import dsp.valueobjects.Iri import dsp.valueobjects.UuidUtil import org.knora.webapi.* import org.knora.webapi.config.AppConfig @@ -124,25 +123,6 @@ case class ResourceVersionHistoryGetRequestV2( requestingUser: User, ) extends ResourcesResponderRequestV2 -/** - * Requests the full version history of a resource and its values as events. - * - * @param resourceIri the IRI of the resource. - * @param requestingUser the user making the request. - */ -case class ResourceHistoryEventsGetRequestV2( - resourceIri: IRI, - requestingUser: User, -) extends ResourcesResponderRequestV2 { - private val stringFormatter = StringFormatter.getInstanceForConstantOntologies - Iri - .validateAndEscapeIri(resourceIri) - .getOrElse(throw BadRequestException(s"Invalid resource IRI: $resourceIri")) - if (!stringFormatter.toSmartIri(resourceIri).isKnoraResourceIri) { - throw BadRequestException(s"Given IRI is not a resource IRI: $resourceIri") - } -} - /** * Requests the version history of all resources of a project. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index b782c75e20..3b6463e8dc 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -184,9 +184,6 @@ final case class ResourcesResponderV2( case resourceIIIFManifestRequest: ResourceIIIFManifestGetRequestV2 => getIIIFManifestV2(resourceIIIFManifestRequest) - case resourceHistoryEventsRequest: ResourceHistoryEventsGetRequestV2 => - getResourceHistoryEvents(resourceHistoryEventsRequest) - case projectHistoryEventsRequestV2: ProjectResourcesWithHistoryGetRequestV2 => getProjectResourceHistoryEvents(projectHistoryEventsRequestV2) @@ -1546,25 +1543,20 @@ final case class ResourcesResponderV2( * @param resourceHistoryEventsGetRequest the request for events describing history of a resource. * @return the events extracted from full representation of a resource at each time point in its history ordered by version date. */ - private def getResourceHistoryEvents( - resourceHistoryEventsGetRequest: ResourceHistoryEventsGetRequestV2, + def getResourceHistoryEvents( + resourceIri: IRI, + requestingUser: User, ): Task[ResourceAndValueVersionHistoryResponseV2] = for { - resourceHistory <- - getResourceHistoryV2( - ResourceVersionHistoryGetRequestV2( - resourceIri = resourceHistoryEventsGetRequest.resourceIri, - withDeletedResource = true, - requestingUser = resourceHistoryEventsGetRequest.requestingUser, - ), - ) - resourceFullHist <- extractEventsFromHistory( - resourceIri = resourceHistoryEventsGetRequest.resourceIri, - resourceHistory = resourceHistory.history, - requestingUser = resourceHistoryEventsGetRequest.requestingUser, - ) - sortedResourceHistory = resourceFullHist.sortBy(_.versionDate) - } yield ResourceAndValueVersionHistoryResponseV2(historyEvents = sortedResourceHistory) + resourceHistory <- getResourceHistoryV2( + ResourceVersionHistoryGetRequestV2( + resourceIri = resourceIri, + withDeletedResource = true, + requestingUser = requestingUser, + ), + ) + resourceFullHist <- extractEventsFromHistory(resourceIri, resourceHistory.history, requestingUser) + } yield ResourceAndValueVersionHistoryResponseV2(resourceFullHist.sortBy(_.versionDate)) /** * Returns events representing the history of all resources and values belonging to a project ordered by date. diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 72527e1095..9ba26cb0d6 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -69,7 +69,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( createResource() ~ updateResourceMetadata() ~ getResourcesInProject() ~ - getResourceHistoryEvents() ~ getProjectResourceAndValueHistory() ~ getResourcesPreview() ~ getResourcesTei() ~ @@ -191,16 +190,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getResourceHistoryEvents(): Route = - path(resourcesBasePath / "resourceHistoryEvents" / Segment) { (resourceIri: IRI) => - get { requestContext => - val requestTask = ZIO - .serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - .map(ResourceHistoryEventsGetRequestV2(resourceIri, _)) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - private def getProjectResourceAndValueHistory(): Route = path(resourcesBasePath / "projectHistoryEvents" / Segment) { (projectIri: IRI) => get { requestContext => diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index 37d857d87e..f1c587d9b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -49,6 +49,12 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { case _ => None }(d => (d, d)) + val getResourcesHistoryEvents = baseEndpoints.withUserEndpoint.get + .in(base / "resourceHistoryEvents" / path[IriDto].name("resourceIri")) + .in(ApiV2.Inputs.formatOptions) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val getResourcesHistory = baseEndpoints.withUserEndpoint.get .in(base / "history" / path[IriDto].name("resourceIri")) .in(ApiV2.Inputs.formatOptions) @@ -65,6 +71,7 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { .out(header[MediaType](HeaderNames.ContentType)) val endpoints: Seq[AnyEndpoint] = Seq( + getResourcesHistoryEvents, getResourcesHistory, getResources, ).map(_.endpoint.tag("V2 Resources")) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index bd5a7615be..9605ba7924 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -18,6 +18,10 @@ final class ResourcesEndpointsHandler( val allHandlers = Seq( + SecuredEndpointHandler( + resourcesEndpoints.getResourcesHistoryEvents, + resourcesRestService.getResourcesHistoryEvents, + ), SecuredEndpointHandler(resourcesEndpoints.getResourcesHistory, resourcesRestService.getResourceHistory), SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), ).map(mapper.mapSecuredEndpointHandler(_)) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index fd4420db02..5d7289488c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -17,6 +17,13 @@ import org.knora.webapi.slice.resources.api.model.VersionDate final case class ResourcesRestService(resourcesService: ResourcesResponderV2, renderer: KnoraResponseRenderer) { + def getResourcesHistoryEvents( + user: User, + )(resourceIri: IriDto, formatOptions: FormatOptions): Task[(RenderedResponse, MediaType)] = + resourcesService + .getResourceHistoryEvents(resourceIri.value, user) + .flatMap(renderer.render(_, formatOptions)) + def getResourceHistory(user: User)( resourceIri: IriDto, formatOptions: FormatOptions, From c92183acdab1bf8a61f68244c24e2bd1f0237130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 12:36:08 +0100 Subject: [PATCH 04/15] Migrate GET v2/resources/projectHistoryEvents/:resourceIri --- .../ResourcesMessagesV2Spec.scala | 39 ------------------- .../v2/ResourcesResponderV2Spec.scala | 33 ++++++++-------- .../resourcemessages/ResourceMessagesV2.scala | 14 ------- .../responders/v2/ResourcesResponderV2.scala | 29 +++++--------- .../knora/webapi/routing/UnsafeZioRun.scala | 6 --- .../webapi/routing/v2/ResourcesRouteV2.scala | 12 ------ .../slice/admin/api/AdminPathVariables.scala | 2 +- .../knora/webapi/slice/admin/api/Codecs.scala | 3 +- .../admin/api/model/AdminQueryVariables.scala | 2 +- .../admin/domain/model/KnoraProject.scala | 3 ++ .../api/ResourceInfoEndpoints.scala | 1 - .../resources/api/ResourcesEndpoints.scala | 8 ++++ .../api/ResourcesEndpointsHandler.scala | 4 ++ .../api/service/ResourcesRestService.scala | 8 ++++ 14 files changed, 52 insertions(+), 112 deletions(-) delete mode 100644 integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala diff --git a/integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala b/integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala deleted file mode 100644 index 734c16b6cd..0000000000 --- a/integration/src/test/scala/org/knora/webapi/messages/v2/responder/resourcesmessages/ResourcesMessagesV2Spec.scala +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.messages.v2.responder.resourcesmessages - -import dsp.errors.BadRequestException -import org.knora.webapi.CoreSpec -import org.knora.webapi.messages.v2.responder.resourcemessages.* -import org.knora.webapi.sharedtestdata.* - -/** - * Tests [[ResourceMessagesV2]]. - */ -class ResourcesMessagesV2Spec extends CoreSpec { - "Get history events of all resources of a project" should { - "fail if given project IRI is not valid" in { - val projectIri = "invalid-project-IRI" - val caught = intercept[BadRequestException]( - ProjectResourcesWithHistoryGetRequestV2( - projectIri = projectIri, - requestingUser = SharedTestDataADM.imagesUser01, - ), - ) - assert(caught.getMessage === s"Invalid project IRI: $projectIri") - } - - "fail if given IRI is not a project Iri" in { - val caught = intercept[BadRequestException]( - ProjectResourcesWithHistoryGetRequestV2( - projectIri = "http://rdfh.ch/0001/thing-with-history", // resource IRI instead of project IRI - requestingUser = SharedTestDataADM.imagesUser01, - ), - ) - assert(caught.getMessage === "Invalid project IRI: http://rdfh.ch/0001/thing-with-history") - } - } -} diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index a9a0bbdb97..f7b680bce7 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -38,6 +38,7 @@ import org.knora.webapi.models.filemodels.* import org.knora.webapi.responders.v2.ResourcesResponseCheckerV2.compareReadResourcesSequenceV2Response import org.knora.webapi.routing.UnsafeZioRun import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.resources.IiifImageRequestUrl import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -2567,9 +2568,7 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => expectMsgType[UpdateResourceMetadataResponseV2](timeout) val events = UnsafeZioRun.runOrThrow( - ZIO.serviceWithZIO[ResourcesResponderV2]( - _.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents), - ), + resourcesResponderV2(_.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents)), ) events.size should be(2) val updateMetadataEvent: Option[ResourceAndValueHistoryEvent] = @@ -2578,25 +2577,27 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => } "not return resources of a project which does not exist" in { - - appActor ! ProjectResourcesWithHistoryGetRequestV2( - projectIri = "http://rdfh.ch/projects/1111", - requestingUser = SharedTestDataADM.anythingAdminUser, + val exit = UnsafeZioRun.run( + resourcesResponderV2( + _.getProjectResourceHistoryEvents( + ProjectIri.unsafeFrom("http://rdfh.ch/projects/1111"), + SharedTestDataADM.anythingAdminUser, + ), + ), ) - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[NotFoundException] should ===(true) - } + assertFailsWithA[NotFoundException](exit) } "return seq of full history events for each resource of a project" in { - appActor ! ProjectResourcesWithHistoryGetRequestV2( - projectIri = "http://rdfh.ch/projects/0001", - requestingUser = SharedTestDataADM.anythingAdminUser, + val response = UnsafeZioRun.runOrThrow( + resourcesResponderV2( + _.getProjectResourceHistoryEvents( + ProjectIri.unsafeFrom("http://rdfh.ch/projects/0001"), + SharedTestDataADM.anythingAdminUser, + ), + ), ) - val response: ResourceAndValueVersionHistoryResponseV2 = - expectMsgType[ResourceAndValueVersionHistoryResponseV2](timeout) response.historyEvents.size should be > 1 - } } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 1a88162499..8882af1f3d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -27,7 +27,6 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStand import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optics.* import org.knora.webapi.slice.admin.api.model.Project -import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.Permission import org.knora.webapi.slice.admin.domain.model.User @@ -123,19 +122,6 @@ case class ResourceVersionHistoryGetRequestV2( requestingUser: User, ) extends ResourcesResponderRequestV2 -/** - * Requests the version history of all resources of a project. - * - * @param projectIri the IRI of the project. - * @param requestingUser the user making the request. - */ -case class ProjectResourcesWithHistoryGetRequestV2( - projectIri: IRI, - requestingUser: User, -) extends ResourcesResponderRequestV2 { - ProjectIri.from(projectIri).getOrElse(throw BadRequestException(s"Invalid project IRI: $projectIri")) -} - /** * Represents an item in the version history of a resource. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 3b6463e8dc..6958fed0cf 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -184,9 +184,6 @@ final case class ResourcesResponderV2( case resourceIIIFManifestRequest: ResourceIIIFManifestGetRequestV2 => getIIIFManifestV2(resourceIIIFManifestRequest) - case projectHistoryEventsRequestV2: ProjectResourcesWithHistoryGetRequestV2 => - getProjectResourceHistoryEvents(projectHistoryEventsRequestV2) - case other => Responder.handleUnexpectedMessage(other, this.getClass.getName) } @@ -1561,20 +1558,17 @@ final case class ResourcesResponderV2( /** * Returns events representing the history of all resources and values belonging to a project ordered by date. * - * @param projectResourceHistoryEventsGetRequest the request for history events of a project. + * @param projectId the history events of a project. + * @param requestingUser the user making the request. + * * @return the all history events of resources of a project ordered by version date. */ - private def getProjectResourceHistoryEvents( - projectResourceHistoryEventsGetRequest: ProjectResourcesWithHistoryGetRequestV2, + def getProjectResourceHistoryEvents( + projectId: ProjectIri, + requestingUser: User, ): Task[ResourceAndValueVersionHistoryResponseV2] = for { - // Get the project; checks if a project with given IRI exists. - projectId <- ZIO - .fromEither(ProjectIri.from(projectResourceHistoryEventsGetRequest.projectIri)) - .mapError(e => BadRequestException(e)) - _ <- knoraProjectService - .findById(projectId) - .someOrFail(NotFoundException(s"Project ${projectId.value} not found")) + _ <- knoraProjectService.findById(projectId).someOrFail(NotFoundException(s"Project $projectId not found")) // Do a SELECT prequery to get the IRIs of the resources that belong to the project. prequery = sparql.v2.txt.getAllResourcesInProjectPrequery(projectId.value) @@ -1589,15 +1583,10 @@ final case class ResourcesResponderV2( ResourceVersionHistoryGetRequestV2( resourceIri = resourceIri, withDeletedResource = true, - requestingUser = projectResourceHistoryEventsGetRequest.requestingUser, + requestingUser = requestingUser, ), ) - resourceFullHist <- - extractEventsFromHistory( - resourceIri = resourceIri, - resourceHistory = resourceHistory.history, - requestingUser = projectResourceHistoryEventsGetRequest.requestingUser, - ) + resourceFullHist <- extractEventsFromHistory(resourceIri, resourceHistory.history, requestingUser) } yield resourceFullHist } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala b/webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala index c824afdbe0..5acb6e9362 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala @@ -31,12 +31,6 @@ object UnsafeZioRun { def runOrThrow[R, E, A](effect: ZIO[R, E, A])(implicit r: Runtime[R]): A = Unsafe.unsafe(implicit u => r.unsafe.run(effect).getOrThrowFiberFailure()) - def runOrThrowWithService[R: Tag, A](run: R => A)(implicit runtime: Runtime[R]): A = - UnsafeZioRun.runOrThrow(ZIO.serviceWith[R](run)) - - def runOrThrowWithServiceZIO[R: Tag, A](run: R => Task[A])(implicit runtime: Runtime[R]): A = - UnsafeZioRun.runOrThrow(ZIO.serviceWithZIO[R](run)) - /** * Executes the effect synchronously and returns its result as a * [[Future]]. diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 9ba26cb0d6..4b7d386f42 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -69,7 +69,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( createResource() ~ updateResourceMetadata() ~ getResourcesInProject() ~ - getProjectResourceAndValueHistory() ~ getResourcesPreview() ~ getResourcesTei() ~ getResourcesGraph() ~ @@ -190,17 +189,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getProjectResourceAndValueHistory(): Route = - path(resourcesBasePath / "projectHistoryEvents" / Segment) { (projectIri: IRI) => - get { requestContext => - val requestTask = - ZIO - .serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - .map(ProjectResourcesWithHistoryGetRequestV2(projectIri, _)) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - private def getResourceIris(resIris: Seq[IRI]): IO[BadRequestException, Seq[IRI]] = ZIO .fail(BadRequestException(s"List of provided resource Iris exceeds limit of $resultsPerPage")) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala index a2f58531ed..07b2ed20a6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminPathVariables.scala @@ -28,7 +28,7 @@ object AdminPathVariables { .example(PermissionIri.unsafeFrom("http://rdfh.ch/permissions/00FF/Mck2xJDjQ_Oimi_9z4aFaA")) val projectIri: EndpointInput.PathCapture[ProjectIri] = - path[ProjectIri](TapirCodec.projectIri) + path[ProjectIri] .name("projectIri") .description("The IRI of a project. Must be URL-encoded.") .example(ProjectIri.unsafeFrom("http://rdfh.ch/projects/0001")) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala index c754302709..be507621a7 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/Codecs.scala @@ -33,7 +33,7 @@ object Codecs { object TapirCodec { type StringCodec[A] = Codec[String, A, CodecFormat.TextPlain] - private def stringCodec[A <: StringValue](from: String => Either[String, A]): StringCodec[A] = + def stringCodec[A <: StringValue](from: String => Either[String, A]): StringCodec[A] = stringCodec(from, _.value) def stringCodec[A](from: String => Either[String, A], to: A => String): StringCodec[A] = Codec.string.mapEither(from)(to) @@ -48,7 +48,6 @@ object Codecs { implicit val logo: StringCodec[Logo] = stringCodec(Logo.from) implicit val longname: StringCodec[Longname] = stringCodec(Longname.from) implicit val selfJoin: StringCodec[SelfJoin] = booleanCodec(SelfJoin.from) - implicit val projectIri: StringCodec[ProjectIri] = stringCodec(ProjectIri.from) implicit val shortcode: StringCodec[Shortcode] = stringCodec(Shortcode.from) implicit val shortname: StringCodec[Shortname] = stringCodec(Shortname.from) implicit val sparqlEncodedString: StringCodec[SparqlEncodedString] = stringCodec(SparqlEncodedString.from) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala index d90baf013c..a69b8feb62 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala @@ -19,7 +19,7 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode object AdminQueryVariables { private implicit val projectIriOptionCodec: Codec[List[String], Option[ProjectIri], CodecFormat.TextPlain] = - Codec.listHeadOption(TapirCodec.projectIri) + Codec.listHeadOption private implicit val shortcodeOptionCodec: Codec[List[String], Option[Shortcode], CodecFormat.TextPlain] = Codec.listHeadOption(TapirCodec.shortcode) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala index 77f085a10d..c27603db53 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala @@ -15,6 +15,7 @@ import dsp.valueobjects.IriErrorMessages import dsp.valueobjects.UuidUtil import org.knora.webapi.messages.StringFormatter.IriDomain import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.slice.admin.api.Codecs.TapirCodec import org.knora.webapi.slice.admin.domain.model.KnoraProject.* import org.knora.webapi.slice.admin.repo.service.EntityWithId import org.knora.webapi.slice.common.StringValueCompanion @@ -46,6 +47,8 @@ object KnoraProject { object ProjectIri extends StringValueCompanion[ProjectIri] { + given TapirCodec.StringCodec[ProjectIri] = TapirCodec.stringCodec(ProjectIri.from) + private val BuiltInProjects: Seq[String] = Seq( "http://www.knora.org/ontology/knora-admin#SystemProject", diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala index 9c06a7e37c..80188520f1 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoEndpoints.scala @@ -10,7 +10,6 @@ import sttp.tapir.generic.auto.* import sttp.tapir.json.zio.* import zio.ZLayer -import org.knora.webapi.slice.admin.api.Codecs.TapirCodec.projectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.common.api.BaseEndpoints diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index f1c587d9b4..95e82c2076 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -15,6 +15,7 @@ import zio.ZLayer import scala.concurrent.Future import dsp.errors.RequestRejectedException +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.common.api.BaseEndpoints @@ -49,6 +50,12 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { case _ => None }(d => (d, d)) + val getResourcesProjectHistoryEvents = baseEndpoints.withUserEndpoint.get + .in(base / "projectHistoryEvents" / path[ProjectIri].name("projectIri")) + .in(ApiV2.Inputs.formatOptions) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val getResourcesHistoryEvents = baseEndpoints.withUserEndpoint.get .in(base / "resourceHistoryEvents" / path[IriDto].name("resourceIri")) .in(ApiV2.Inputs.formatOptions) @@ -71,6 +78,7 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { .out(header[MediaType](HeaderNames.ContentType)) val endpoints: Seq[AnyEndpoint] = Seq( + getResourcesProjectHistoryEvents, getResourcesHistoryEvents, getResourcesHistory, getResources, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index 9605ba7924..bb2d1a1582 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -18,6 +18,10 @@ final class ResourcesEndpointsHandler( val allHandlers = Seq( + SecuredEndpointHandler( + resourcesEndpoints.getResourcesProjectHistoryEvents, + resourcesRestService.getResourcesProjectHistoryEvents, + ), SecuredEndpointHandler( resourcesEndpoints.getResourcesHistoryEvents, resourcesRestService.getResourcesHistoryEvents, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index 5d7289488c..5b964e3872 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -8,6 +8,7 @@ import sttp.model.MediaType import zio.* import org.knora.webapi.responders.v2.ResourcesResponderV2 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions @@ -17,6 +18,13 @@ import org.knora.webapi.slice.resources.api.model.VersionDate final case class ResourcesRestService(resourcesService: ResourcesResponderV2, renderer: KnoraResponseRenderer) { + def getResourcesProjectHistoryEvents( + user: User, + )(projectIri: ProjectIri, formatOptions: FormatOptions): Task[(RenderedResponse, MediaType)] = + resourcesService + .getProjectResourceHistoryEvents(projectIri, user) + .flatMap(renderer.render(_, formatOptions)) + def getResourcesHistoryEvents( user: User, )(resourceIri: IriDto, formatOptions: FormatOptions): Task[(RenderedResponse, MediaType)] = From 7fc5f45a2e03fdadd956d2672b56516bad5694af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 12:52:08 +0100 Subject: [PATCH 05/15] Migrate GET v2/resources/iiifmanifest/:resourceIri --- .../resourcemessages/ResourceMessagesV2.scala | 12 ------------ .../responders/v2/ResourcesResponderV2.scala | 17 +++++++---------- .../webapi/routing/v2/ResourcesRouteV2.scala | 17 +---------------- .../resources/api/ResourcesEndpoints.scala | 7 +++++++ .../api/ResourcesEndpointsHandler.scala | 4 ++++ .../api/service/ResourcesRestService.scala | 8 ++++++++ 6 files changed, 27 insertions(+), 38 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 8882af1f3d..629e742af1 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -79,18 +79,6 @@ case class ResourcesPreviewGetRequestV2( requestingUser: User, ) extends ResourcesResponderRequestV2 -/** - * Requests a IIIF manifest for the images that are `knora-base:isPartOf` the specified - * resource. - * - * @param resourceIri the resource IRI. - * @param requestingUser the user making the request. - */ -case class ResourceIIIFManifestGetRequestV2( - resourceIri: IRI, - requestingUser: User, -) extends ResourcesResponderRequestV2 - /** * Represents a IIIF manifest for the images that are `knora-base:isPartOf` the specified * resource. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 6958fed0cf..f7584d7460 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -182,8 +182,6 @@ final case class ResourcesResponderV2( case resourceHistoryRequest: ResourceVersionHistoryGetRequestV2 => getResourceHistoryV2(resourceHistoryRequest) - case resourceIIIFManifestRequest: ResourceIIIFManifestGetRequestV2 => getIIIFManifestV2(resourceIIIFManifestRequest) - case other => Responder.handleUnexpectedMessage(other, this.getClass.getName) } @@ -1408,7 +1406,7 @@ final case class ResourcesResponderV2( historyEntriesWithResourceCreation, ) - private def getIIIFManifestV2(request: ResourceIIIFManifestGetRequestV2): Task[ResourceIIIFManifestGetResponseV2] = + def getIiifManifestV2(resourceIri: IRI, requestingUser: User): Task[ResourceIIIFManifestGetResponseV2] = // The implementation here is experimental. If we had a way of streaming the canvas URLs to the IIIF viewer, // it would be better to write the Gravsearch query differently, so that ?representation was the main resource. // Then the Gravsearch query could return pages of results. @@ -1420,9 +1418,7 @@ final case class ResourcesResponderV2( // Make a Gravsearch query from a template. gravsearchQueryForIncomingLinks <- ZIO.attempt( org.knora.webapi.messages.twirl.queries.gravsearch.txt - .getIncomingImageLinks( - resourceIri = request.resourceIri, - ) + .getIncomingImageLinks(resourceIri) .toString(), ) @@ -1432,10 +1428,10 @@ final case class ResourcesResponderV2( searchResponse <- searchResponderV2.gravsearchV2( parsedGravsearchQuery, apiV2SchemaWithOption(MarkupRendering.Standoff), - request.requestingUser, + requestingUser, ) - resource = searchResponse.toResource(request.resourceIri) + resource = searchResponse.toResource(resourceIri) incomingLinks = resource.values.getOrElse(OntologyConstants.KnoraBase.HasIncomingLinkValue.toSmartIri, Seq.empty) representations: Seq[ReadResourceV2] = incomingLinks.collect { case readLinkValueV2: ReadLinkValueV2 => @@ -1447,7 +1443,7 @@ final case class ResourcesResponderV2( body = JsonLDObject( Map( JsonLDKeywords.CONTEXT -> JsonLDString("http://iiif.io/api/presentation/3/context.json"), - "id" -> JsonLDString(s"${request.resourceIri}/manifest"), // Is this IRI OK? + "id" -> JsonLDString(s"${resourceIri}/manifest"), // Is this IRI OK? "type" -> JsonLDString("Manifest"), "label" -> JsonLDObject(Map("en" -> JsonLDArray(Seq(JsonLDString(resource.label))))), "behavior" -> JsonLDArray(Seq(JsonLDString("paged"))), @@ -1537,7 +1533,8 @@ final case class ResourcesResponderV2( /** * Returns all events describing the history of a resource ordered by version date. * - * @param resourceHistoryEventsGetRequest the request for events describing history of a resource. + * @param resourceIri the request for events describing history of a resource. + * @param requestingUser the user making the request. * @return the events extracted from full representation of a resource at each time point in its history ordered by version date. */ def getResourceHistoryEvents( diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 4b7d386f42..51c4a971fe 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -65,8 +65,7 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( private val Both = "both" def makeRoute: Route = - getIIIFManifest() ~ - createResource() ~ + createResource() ~ updateResourceMetadata() ~ getResourcesInProject() ~ getResourcesPreview() ~ @@ -75,20 +74,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( deleteResource() ~ eraseResource() - private def getIIIFManifest(): Route = - path(resourcesBasePath / "iiifmanifest" / Segment) { (resourceIriStr: IRI) => - get { requestContext => - val requestTask = for { - resourceIri <- Iri - .validateAndEscapeIri(resourceIriStr) - .toZIO - .orElseFail(BadRequestException(s"Invalid resource IRI: $resourceIriStr")) - user <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - } yield ResourceIIIFManifestGetRequestV2(resourceIri, user) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - private def createResource(): Route = path(resourcesBasePath) { post { entity(as[String]) { jsonRequest => requestContext => diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index 95e82c2076..e4ae1d2fe8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -50,6 +50,12 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { case _ => None }(d => (d, d)) + val getResourcesIiifManifest = baseEndpoints.withUserEndpoint.get + .in(base / "iiifmanifest" / path[IriDto].name("resourceIri")) + .in(ApiV2.Inputs.formatOptions) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val getResourcesProjectHistoryEvents = baseEndpoints.withUserEndpoint.get .in(base / "projectHistoryEvents" / path[ProjectIri].name("projectIri")) .in(ApiV2.Inputs.formatOptions) @@ -78,6 +84,7 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { .out(header[MediaType](HeaderNames.ContentType)) val endpoints: Seq[AnyEndpoint] = Seq( + getResourcesIiifManifest, getResourcesProjectHistoryEvents, getResourcesHistoryEvents, getResourcesHistory, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index bb2d1a1582..d58378341a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -18,6 +18,10 @@ final class ResourcesEndpointsHandler( val allHandlers = Seq( + SecuredEndpointHandler( + resourcesEndpoints.getResourcesIiifManifest, + resourcesRestService.getResourcesIiifManifest, + ), SecuredEndpointHandler( resourcesEndpoints.getResourcesProjectHistoryEvents, resourcesRestService.getResourcesProjectHistoryEvents, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index 5b964e3872..889d9abec1 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -18,6 +18,14 @@ import org.knora.webapi.slice.resources.api.model.VersionDate final case class ResourcesRestService(resourcesService: ResourcesResponderV2, renderer: KnoraResponseRenderer) { + def getResourcesIiifManifest(user: User)( + resourceIri: IriDto, + formatOptions: FormatOptions, + ): Task[(RenderedResponse, MediaType)] = + resourcesService + .getIiifManifestV2(resourceIri.value, user) + .flatMap(renderer.render(_, formatOptions)) + def getResourcesProjectHistoryEvents( user: User, )(projectIri: ProjectIri, formatOptions: FormatOptions): Task[(RenderedResponse, MediaType)] = From 0a42a18ff26d763ef5946226617bac3c8062fef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 14:18:01 +0100 Subject: [PATCH 06/15] return failed zio instead of throwing exception --- .../responders/v2/SearchResponderV2Spec.scala | 34 +++------ .../resourcemessages/ResourceMessagesV2.scala | 18 +++-- .../responders/v2/ResourcesResponderV2.scala | 7 +- .../responders/v2/SearchResponderV2.scala | 6 +- .../webapi/routing/v2/ResourcesRouteV2.scala | 70 ------------------- .../resources/api/ResourcesApiModule.scala | 6 +- .../resources/api/ResourcesEndpoints.scala | 11 +++ .../api/ResourcesEndpointsHandler.scala | 4 ++ .../api/service/ResourcesRestService.scala | 28 +++++++- 9 files changed, 72 insertions(+), 112 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala index 746c8d9985..10db91d914 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala @@ -95,24 +95,6 @@ class SearchResponderV2Spec extends CoreSpec { private def gravsearchV2(query: ConstructQuery, schemaAndOptions: SchemaRendering, user: User) = ZIO.serviceWithZIO[SearchResponderV2](_.gravsearchV2(query, schemaAndOptions, user)) - private def searchResourcesByProjectAndClassV2( - projectIri: SmartIri, - resourceClass: SmartIri, - orderByProperty: Option[SmartIri], - page: Int, - schemaAndOptions: SchemaRendering, - requestingUser: User, - ) = ZIO.serviceWithZIO[SearchResponderV2]( - _.searchResourcesByProjectAndClassV2( - projectIri, - resourceClass, - orderByProperty, - page, - schemaAndOptions, - requestingUser, - ), - ) - "The search responder v2" should { "perform a fulltext search for 'Narr'" in { @@ -302,13 +284,15 @@ class SearchResponderV2Spec extends CoreSpec { "search by project and resource class" in { val result = UnsafeZioRun.runOrThrow( - searchResourcesByProjectAndClassV2( - projectIri = SharedTestDataADM.incunabulaProject.id.value.toSmartIri, - resourceClass = "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book".toSmartIri, - orderByProperty = Some("http://0.0.0.0:3333/ontology/0803/incunabula/v2#title".toSmartIri), - page = 0, - schemaAndOptions = SchemaRendering.apiV2SchemaWithOption(MarkupRendering.Xml), - requestingUser = SharedTestDataADM.incunabulaProjectAdminUser, + ZIO.serviceWithZIO[SearchResponderV2]( + _.searchResourcesByProjectAndClassV2( + projectIri = SharedTestDataADM.incunabulaProject.id, + resourceClass = "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book".toSmartIri, + orderByProperty = Some("http://0.0.0.0:3333/ontology/0803/incunabula/v2#title".toSmartIri), + page = 0, + schemaAndOptions = SchemaRendering.apiV2SchemaWithOption(MarkupRendering.Xml), + requestingUser = SharedTestDataADM.incunabulaProjectAdminUser, + ), ), ) result.resources.size should ===(19) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 629e742af1..61de24940b 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -7,7 +7,6 @@ package org.knora.webapi.messages.v2.responder.resourcemessages import java.time.Instant import java.util.UUID - import dsp.errors.* import dsp.valueobjects.UuidUtil import org.knora.webapi.* @@ -29,6 +28,8 @@ import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optic import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.domain.model.Permission import org.knora.webapi.slice.admin.domain.model.User +import zio.Task +import zio.ZIO /** * An abstract trait for messages that can be sent to `ResourcesResponderV2`. @@ -863,22 +864,27 @@ case class ReadResourcesSequenceV2( * @throws NotFoundException if the requested resources are not found. * @throws ForbiddenException if the user does not have permission to see the requested resources. */ - def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Unit = { + def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Task[Unit] = { val hiddenTargetResourceIris: Set[IRI] = targetResourceIris.intersect(resourcesSequence.hiddenResourceIris) if (hiddenTargetResourceIris.nonEmpty) { - throw ForbiddenException( - s"You do not have permission to see one or more resources: ${hiddenTargetResourceIris.map(iri => s"<$iri>").mkString(", ")}", + return ZIO.fail( + ForbiddenException( + s"You do not have permission to see one or more resources: ${hiddenTargetResourceIris.map(iri => s"<$iri>").mkString(", ")}", + ), ) } val missingResourceIris: Set[IRI] = targetResourceIris -- resourcesSequence.resources.map(_.resourceIri).toSet if (missingResourceIris.nonEmpty) { - throw NotFoundException( - s"One or more resources were not found: ${missingResourceIris.map(iri => s"<$iri>").mkString(", ")}", + return ZIO.fail( + NotFoundException( + s"One or more resources were not found: ${missingResourceIris.map(iri => s"<$iri>").mkString(", ")}", + ), ) } + ZIO.unit } /** diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index f7584d7460..73ecd195ba 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -620,7 +620,7 @@ final case class ResourcesResponderV2( requestingUser = requestingUser, ) - _ = apiResponse.checkResourceIris(resourceIris.toSet, apiResponse) + _ <- apiResponse.checkResourceIris(resourceIris.toSet, apiResponse) _ <- valueUuid match { case Some(definedValueUuid) => @@ -693,10 +693,7 @@ final case class ResourcesResponderV2( requestingUser = requestingUser, ) - _ = apiResponse.checkResourceIris( - targetResourceIris = resourceIris.toSet, - resourcesSequence = apiResponse, - ) + _ <- apiResponse.checkResourceIris(resourceIris.toSet, apiResponse) // Check if resources are deleted, if so, replace them with DeletedResource responseWithDeletedResourcesReplaced = apiResponse.resources match { diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index cb27b911c5..f5cc025dc2 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -205,7 +205,7 @@ trait SearchResponderV2 { * @return a [[ReadResourcesSequenceV2]]. */ def searchResourcesByProjectAndClassV2( - projectIri: SmartIri, + projectIri: ProjectIri, resourceClass: SmartIri, orderByProperty: Option[SmartIri], page: Int, @@ -682,7 +682,7 @@ final case class SearchResponderV2Live( * @return a [[ReadResourcesSequenceV2]]. */ override def searchResourcesByProjectAndClassV2( - projectIri: SmartIri, + projectIri: ProjectIri, resourceClass: SmartIri, orderByProperty: Option[SmartIri], page: Int, @@ -776,7 +776,7 @@ final case class SearchResponderV2Live( // Do a SELECT prequery to get the IRIs of the requested page of resources. prequery = sparql.v2.txt .getResourcesByClassInProjectPrequery( - projectIri = projectIri.toString, + projectIri = projectIri.value, resourceClassIri = internalClassIri, maybeOrderByProperty = maybeInternalOrderByPropertyIri, maybeOrderByValuePredicate = maybeOrderByValuePredicate, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 51c4a971fe..c640a14f62 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -30,7 +30,6 @@ import org.knora.webapi.routing.RouteUtilZ import org.knora.webapi.slice.admin.domain.service.ProjectService import org.knora.webapi.slice.admin.domain.service.UserService import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser -import org.knora.webapi.slice.common.api.ApiV2.Headers.xKnoraAcceptProject import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.slice.security.Authenticator import org.knora.webapi.store.iiif.api.SipiService @@ -67,7 +66,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( def makeRoute: Route = createResource() ~ updateResourceMetadata() ~ - getResourcesInProject() ~ getResourcesPreview() ~ getResourcesTei() ~ getResourcesGraph() ~ @@ -106,74 +104,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getResourcesInProject(): Route = path(resourcesBasePath) { - get { requestContext => - val params: Map[String, String] = requestContext.request.uri.query().toMap - - val getResourceClass = ZIO - .fromOption(params.get("resourceClass")) - .orElseFail(BadRequestException(s"This route requires the parameter 'resourceClass'")) - .flatMap(iri => - ZIO - .serviceWithZIO[IriConverter](_.asSmartIri(iri)) - .orElseFail(BadRequestException(s"Invalid resource class IRI: $iri")), - ) - .filterOrElseWith(it => it.isKnoraApiV2EntityIri && it.isApiV2ComplexSchema)(it => - ZIO.fail(BadRequestException(s"Invalid resource class IRI: $it")), - ) - .flatMap(it => ZIO.serviceWithZIO[IriConverter](_.asInternalSmartIri(it))) - - val getOrderByProperty: ZIO[IriConverter, Throwable, Option[SmartIri]] = - ZIO.foreach(params.get("orderByProperty")) { orderByPropertyStr => - ZIO - .serviceWithZIO[IriConverter](_.asSmartIri(orderByPropertyStr)) - .orElseFail(BadRequestException(s"Invalid property IRI: $orderByPropertyStr")) - .filterOrFail(iri => iri.isKnoraApiV2EntityIri && iri.isApiV2ComplexSchema)( - BadRequestException(s"Invalid property IRI: $orderByPropertyStr"), - ) - .flatMap(it => ZIO.serviceWithZIO[IriConverter](_.asInternalSmartIri(it))) - } - - val getPage = ZIO - .fromOption(params.get("page")) - .orElseFail(BadRequestException(s"This route requires the parameter 'page'")) - .flatMap(pageStr => - ZIO - .fromOption(ValuesValidator.validateInt(pageStr)) - .orElseFail(BadRequestException(s"Invalid page number: $pageStr")), - ) - - val getProjectIri = RouteUtilV2 - .getProjectIri(requestContext) - .some - .orElseFail(BadRequestException(s"This route requires the request header $xKnoraAcceptProject")) - - val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) - val response = for { - maybeOrderByProperty <- getOrderByProperty - resourceClass <- getResourceClass - projectIri <- getProjectIri - page <- getPage - targetSchema <- targetSchemaTask.zip(RouteUtilV2.getSchemaOptions(requestContext)).map { - case (schema, options) => SchemaRendering(schema, options) - } - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - response <- ZIO.serviceWithZIO[SearchResponderV2]( - _.searchResourcesByProjectAndClassV2( - projectIri, - resourceClass, - maybeOrderByProperty, - page, - targetSchema, - requestingUser, - ), - ) - } yield response - - RouteUtilV2.completeResponse(response, requestContext, targetSchemaTask) - } - } - private def getResourceIris(resIris: Seq[IRI]): IO[BadRequestException, Seq[IRI]] = ZIO .fail(BadRequestException(s"List of provided resource Iris exceeds limit of $resultsPerPage")) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala index cdc23bfba7..da98af8812 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala @@ -9,6 +9,7 @@ import zio.URLayer import zio.ZLayer import org.knora.webapi.responders.v2.ResourcesResponderV2 +import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.responders.v2.ValuesResponderV2 import org.knora.webapi.slice.URModule import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser @@ -16,13 +17,14 @@ import org.knora.webapi.slice.common.api.BaseEndpoints import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter +import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.slice.resources.api.service.ResourcesRestService import org.knora.webapi.slice.resources.api.service.ValuesRestService object ResourcesApiModule extends URModule[ - ApiComplexV2JsonLdRequestParser & BaseEndpoints & HandlerMapper & KnoraResponseRenderer & ResourcesResponderV2 & - TapirToPekkoInterpreter & ValuesResponderV2, + ApiComplexV2JsonLdRequestParser & BaseEndpoints & HandlerMapper & IriConverter & KnoraResponseRenderer & + ResourcesResponderV2 & SearchResponderV2 & TapirToPekkoInterpreter & ValuesResponderV2, ResourcesApiRoutes & ValuesEndpoints & ResourcesEndpoints, ] { self => diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index e4ae1d2fe8..8b95e17a95 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -83,12 +83,23 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) + val getResourcesParams = baseEndpoints.withUserEndpoint.get + .in(base) + .in(query[IriDto]("resourceClass")) + .in(query[Option[IriDto]]("orderByProperty")) + .in(query[Int]("page").validate(Validator.min(0))) + .in(header[ProjectIri](ApiV2.Headers.xKnoraAcceptProject)) + .in(ApiV2.Inputs.formatOptions) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val endpoints: Seq[AnyEndpoint] = Seq( getResourcesIiifManifest, getResourcesProjectHistoryEvents, getResourcesHistoryEvents, getResourcesHistory, getResources, + getResourcesParams, ).map(_.endpoint.tag("V2 Resources")) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index d58378341a..ed9a0b9685 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -31,6 +31,10 @@ final class ResourcesEndpointsHandler( resourcesRestService.getResourcesHistoryEvents, ), SecuredEndpointHandler(resourcesEndpoints.getResourcesHistory, resourcesRestService.getResourceHistory), + SecuredEndpointHandler( + resourcesEndpoints.getResourcesParams, + resourcesRestService.searchResourcesByProjectAndClass, + ), SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), ).map(mapper.mapSecuredEndpointHandler(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index 889d9abec1..28d45f83cf 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -7,16 +7,24 @@ package org.knora.webapi.slice.resources.api.service import sttp.model.MediaType import zio.* +import dsp.errors.BadRequestException import org.knora.webapi.responders.v2.ResourcesResponderV2 +import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse +import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.slice.resources.api.model.IriDto import org.knora.webapi.slice.resources.api.model.VersionDate -final case class ResourcesRestService(resourcesService: ResourcesResponderV2, renderer: KnoraResponseRenderer) { +final case class ResourcesRestService( + private val resourcesService: ResourcesResponderV2, + private val searchService: SearchResponderV2, + private val iriConverter: IriConverter, + renderer: KnoraResponseRenderer, +) { def getResourcesIiifManifest(user: User)( resourceIri: IriDto, @@ -40,6 +48,24 @@ final case class ResourcesRestService(resourcesService: ResourcesResponderV2, re .getResourceHistoryEvents(resourceIri.value, user) .flatMap(renderer.render(_, formatOptions)) + def searchResourcesByProjectAndClass(user: User)( + resourceClass: IriDto, + orderByProperty: Option[IriDto], + page: Int, + projectIri: ProjectIri, + format: FormatOptions, + ): Task[(RenderedResponse, MediaType)] = for { + resourceClass <- iriConverter + .asResourceClassIri(resourceClass.value) + .mapBoth(BadRequestException.apply, _.smartIri.toInternalSchema) + order <- ZIO + .foreach(orderByProperty.map(_.value))(iriConverter.asPropertyIri) + .mapBoth(BadRequestException.apply, _.map(_.smartIri.toInternalSchema)) + rendering = format.schemaRendering + result <- searchService.searchResourcesByProjectAndClassV2(projectIri, resourceClass, order, page, rendering, user) + response <- renderer.render(result, format) + } yield response + def getResourceHistory(user: User)( resourceIri: IriDto, formatOptions: FormatOptions, From 1ea5c463224f05963ca71c76c45dd03e9680051d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 15:04:54 +0100 Subject: [PATCH 07/15] Add Resources endpoint to ApiV2Endpoints --- .../scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala index 8a491c6a24..76f5140e69 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2Endpoints.scala @@ -28,6 +28,7 @@ final case class ApiV2Endpoints( authenticationEndpoints.endpoints ++ listsEndpointsV2.endpoints ++ resourceInfoEndpoints.endpoints ++ + resourcesEndpoints.endpoints ++ searchEndpoints.endpoints ++ valuesEndpoints.endpoints } From bc9f100e7137904218d536570d95e240dbde609b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 15:07:48 +0100 Subject: [PATCH 08/15] Add Resources endpoint to ApiV2Endpoints --- .../v2/responder/resourcemessages/ResourceMessagesV2.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 61de24940b..7b43c2db3e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -5,8 +5,12 @@ package org.knora.webapi.messages.v2.responder.resourcemessages +import zio.Task +import zio.ZIO + import java.time.Instant import java.util.UUID + import dsp.errors.* import dsp.valueobjects.UuidUtil import org.knora.webapi.* @@ -28,8 +32,6 @@ import org.knora.webapi.messages.v2.responder.valuemessages.ValueMessagesV2Optic import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.domain.model.Permission import org.knora.webapi.slice.admin.domain.model.User -import zio.Task -import zio.ZIO /** * An abstract trait for messages that can be sent to `ResourcesResponderV2`. From 58d742ebd890f371a52991746a3ce2058503fa93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 16:08:09 +0100 Subject: [PATCH 09/15] Migrate GET v2/resources/:resourceIris --- .../webapi/routing/v2/ResourcesRouteV2.scala | 26 ------------- .../resources/api/ResourcesEndpoints.scala | 7 ++++ .../api/ResourcesEndpointsHandler.scala | 4 ++ .../api/service/ResourcesRestService.scala | 38 ++++++++++++------- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index c640a14f62..ac0f9a0f28 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -47,7 +47,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( private val jsonLdRequestParser = ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser] private val sipiConfig: Sipi = appConfig.sipi - private val resultsPerPage: Int = appConfig.v2.resourcesSequence.resultsPerPage private val graphRouteConfig: GraphRoute = appConfig.v2.graphRoute private val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") @@ -66,7 +65,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( def makeRoute: Route = createResource() ~ updateResourceMetadata() ~ - getResourcesPreview() ~ getResourcesTei() ~ getResourcesGraph() ~ deleteResource() ~ @@ -104,30 +102,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getResourceIris(resIris: Seq[IRI]): IO[BadRequestException, Seq[IRI]] = - ZIO - .fail(BadRequestException(s"List of provided resource Iris exceeds limit of $resultsPerPage")) - .when(resIris.size > resultsPerPage) *> - ZIO.foreach(resIris) { (resIri: IRI) => - Iri - .validateAndEscapeIri(resIri) - .toZIO - .orElseFail(BadRequestException(s"Invalid resource IRI: <$resIri>")) - } - - private def getResourcesPreview(): Route = - path("v2" / "resourcespreview" / Segments) { (resIris: Seq[String]) => - get { requestContext => - val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) - val requestTask = for { - resourceIris <- getResourceIris(resIris) - targetSchema <- targetSchemaTask - user <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - } yield ResourcesPreviewGetRequestV2(resourceIris, withDeletedResource = true, targetSchema, user) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext, targetSchemaTask) - } - } - private def getResourcesTei(): Route = path("v2" / "tei" / Segment) { (resIri: String) => get { requestContext => val params: Map[String, String] = requestContext.request.uri.query().toMap diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index 8b95e17a95..f2c695f31f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -50,6 +50,12 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { case _ => None }(d => (d, d)) + val getResourcesPreview = baseEndpoints.withUserEndpoint.get + .in("v2" / "resourcespreview" / paths) + .in(ApiV2.Inputs.formatOptions) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val getResourcesIiifManifest = baseEndpoints.withUserEndpoint.get .in(base / "iiifmanifest" / path[IriDto].name("resourceIri")) .in(ApiV2.Inputs.formatOptions) @@ -95,6 +101,7 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { val endpoints: Seq[AnyEndpoint] = Seq( getResourcesIiifManifest, + getResourcesPreview, getResourcesProjectHistoryEvents, getResourcesHistoryEvents, getResourcesHistory, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index ed9a0b9685..fe3ed54893 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -22,6 +22,10 @@ final class ResourcesEndpointsHandler( resourcesEndpoints.getResourcesIiifManifest, resourcesRestService.getResourcesIiifManifest, ), + SecuredEndpointHandler( + resourcesEndpoints.getResourcesPreview, + resourcesRestService.getResourcesPreview, + ), SecuredEndpointHandler( resourcesEndpoints.getResourcesProjectHistoryEvents, resourcesRestService.getResourcesProjectHistoryEvents, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index 28d45f83cf..272a30d085 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -34,6 +34,15 @@ final case class ResourcesRestService( .getIiifManifestV2(resourceIri.value, user) .flatMap(renderer.render(_, formatOptions)) + def getResourcesPreview(user: User)( + resourceIris: List[String], + formatOptions: FormatOptions, + ): Task[(RenderedResponse, MediaType)] = + ensureIris(resourceIris) *> + resourcesService + .getResourcePreviewV2(resourceIris, withDeleted = true, formatOptions.schema, user) + .flatMap(renderer.render(_, formatOptions)) + def getResourcesProjectHistoryEvents( user: User, )(projectIri: ProjectIri, formatOptions: FormatOptions): Task[(RenderedResponse, MediaType)] = @@ -81,20 +90,23 @@ final case class ResourcesRestService( formatOptions: FormatOptions, version: Option[VersionDate], ): Task[(RenderedResponse, MediaType)] = - resourcesService - .getResourcesV2( - resourceIris, - propertyIri = None, - valueUuid = None, - version.map(_.value), - withDeleted = true, - showDeletedValues = false, - formatOptions.schema, - formatOptions.rendering, - user, - ) - .flatMap(renderer.render(_, formatOptions)) + ensureIris(resourceIris) *> + resourcesService + .getResourcesV2( + resourceIris, + propertyIri = None, + valueUuid = None, + version.map(_.value), + withDeleted = true, + showDeletedValues = false, + formatOptions.schema, + formatOptions.rendering, + user, + ) + .flatMap(renderer.render(_, formatOptions)) + private def ensureIris(values: List[String]): Task[Unit] = + ZIO.foreachDiscard(values)(str => ZIO.fromEither(IriDto.from(str)).mapError(BadRequestException.apply)) } object ResourcesRestService { From 333e06fe04c27dba919a4c71e93e1685568c5205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 18:32:05 +0100 Subject: [PATCH 10/15] Migrate GET v2/graph/:resourceIri --- .../v2/ResourcesResponderV2Spec.scala | 91 ++++++++++--------- .../org/knora/webapi/config/AppConfig.scala | 7 +- .../resourcemessages/ResourceMessagesV2.scala | 24 ----- .../responders/v2/ResourcesResponderV2.scala | 36 ++++---- .../webapi/routing/v2/ResourcesRouteV2.scala | 57 +----------- .../slice/common/api/DocsGenerator.scala | 2 + .../resources/api/ResourcesApiModule.scala | 5 +- .../resources/api/ResourcesEndpoints.scala | 22 ++++- .../api/ResourcesEndpointsHandler.scala | 1 + .../slice/resources/api/model/Models.scala | 22 +++++ .../api/service/ResourcesRestService.scala | 13 +++ 11 files changed, 134 insertions(+), 146 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index f7b680bce7..8eeb2a8f64 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -41,6 +41,7 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.resources.IiifImageRequestUrl +import org.knora.webapi.slice.resources.api.model.GraphDirection import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Ask import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select @@ -799,69 +800,71 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => } "return a graph of resources reachable via links from/to a given resource" in { - appActor ! GraphDataGetRequestV2( - resourceIri = "http://rdfh.ch/0001/start", - depth = 6, - inbound = true, - outbound = true, - excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), - requestingUser = SharedTestDataADM.anythingUser1, + val response = UnsafeZioRun.runOrThrow( + resourcesResponderV2( + _.getGraphDataResponseV2( + resourceIri = "http://rdfh.ch/0001/start", + depth = 6, + GraphDirection.Both, + excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), + requestingUser = SharedTestDataADM.anythingUser1, + ), + ), ) - - val response = expectMsgType[GraphDataGetResponseV2](timeout) - val edges = response.edges - val nodes = response.nodes + val edges = response.edges + val nodes = response.nodes edges should contain theSameElementsAs graphTestData.graphForAnythingUser1.edges nodes should contain theSameElementsAs graphTestData.graphForAnythingUser1.nodes } "return a graph of resources reachable via links from/to a given resource, filtering the results according to the user's permissions" in { - appActor ! GraphDataGetRequestV2( - resourceIri = "http://rdfh.ch/0001/start", - depth = 6, - inbound = true, - outbound = true, - excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), - requestingUser = SharedTestDataADM.incunabulaProjectAdminUser, + val response = UnsafeZioRun.runOrThrow( + resourcesResponderV2( + _.getGraphDataResponseV2( + resourceIri = "http://rdfh.ch/0001/start", + depth = 6, + GraphDirection.Both, + excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), + requestingUser = SharedTestDataADM.incunabulaProjectAdminUser, + ), + ), ) - - val response = expectMsgType[GraphDataGetResponseV2](timeout) - val edges = response.edges - val nodes = response.nodes + val edges = response.edges + val nodes = response.nodes edges should contain theSameElementsAs graphTestData.graphForIncunabulaUser.edges nodes should contain theSameElementsAs graphTestData.graphForIncunabulaUser.nodes } "return a graph containing a standoff link" in { - appActor ! GraphDataGetRequestV2( - resourceIri = "http://rdfh.ch/0001/a-thing", - depth = 4, - inbound = true, - outbound = true, - excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), - requestingUser = SharedTestDataADM.anythingUser1, + val response = UnsafeZioRun.runOrThrow( + resourcesResponderV2( + _.getGraphDataResponseV2( + resourceIri = "http://rdfh.ch/0001/a-thing", + depth = 4, + GraphDirection.Both, + excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), + requestingUser = SharedTestDataADM.anythingUser1, + ), + ), ) - - expectMsgPF(timeout) { case response: GraphDataGetResponseV2 => - response should ===(graphTestData.graphWithStandoffLink) - } + response should ===(graphTestData.graphWithStandoffLink) } "return a graph containing just one node" in { - appActor ! GraphDataGetRequestV2( - resourceIri = "http://rdfh.ch/0001/another-thing", - depth = 4, - inbound = true, - outbound = true, - excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), - requestingUser = SharedTestDataADM.anythingUser1, + val response = UnsafeZioRun.runOrThrow( + resourcesResponderV2( + _.getGraphDataResponseV2( + resourceIri = "http://rdfh.ch/0001/another-thing", + depth = 4, + GraphDirection.Both, + excludeProperty = Some(OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri), + requestingUser = SharedTestDataADM.anythingUser1, + ), + ), ) - - expectMsgPF(timeout) { case response: GraphDataGetResponseV2 => - response should ===(graphTestData.graphWithOneNode) - } + response should ===(graphTestData.graphWithOneNode) } "create a resource with no values" in { diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index c86b2cf01f..841f539d66 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -181,8 +181,8 @@ final case class Features( ) object AppConfig { - type AppConfigurationsTest = AppConfig & DspIngestConfig & Triplestore & Features & Sipi - type AppConfigurations = AppConfigurationsTest & InstrumentationServerConfig & JwtConfig & KnoraApi + type AppConfigurations = AppConfig & DspIngestConfig & Features & GraphRoute & InstrumentationServerConfig & + JwtConfig & KnoraApi & Sipi & Triplestore val parseConfig: UIO[AppConfig] = { val descriptor = deriveConfig[AppConfig].mapKey(toKebabCase) @@ -216,5 +216,6 @@ object AppConfig { val issuerFromConfigOrDefault: Option[String] = jwtConfig.issuer.orElse(Some(appConfig.knoraApi.externalKnoraApiHostPort)) jwtConfig.copy(issuer = issuerFromConfigOrDefault) - } + } ++ + appConfigLayer.project(_.v2.graphRoute) } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 7b43c2db3e..3a7e1ad6f4 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -910,30 +910,6 @@ case class ReadResourcesSequenceV2( } } -/** - * Requests a graph of resources that are reachable via links to or from a given resource. A successful response - * will be a [[GraphDataGetResponseV2]]. - * - * @param resourceIri the IRI of the initial resource. - * @param depth the maximum depth of the graph, counting from the initial resource. - * @param inbound `true` to query inbound links. - * @param outbound `true` to query outbound links. - * @param excludeProperty the IRI of a link property to exclude from the results. - * @param requestingUser the user making the request. - */ -case class GraphDataGetRequestV2( - resourceIri: IRI, - depth: Int, - inbound: Boolean, - outbound: Boolean, - excludeProperty: Option[SmartIri], - requestingUser: User, -) extends ResourcesResponderRequestV2 { - if (!(inbound || outbound)) { - throw BadRequestException("No link direction selected") - } -} - /** * Represents a node (i.e. a resource) in a resource graph. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 73ecd195ba..3b30ba62c8 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -51,6 +51,7 @@ import org.knora.webapi.slice.admin.domain.service.LegalInfoService import org.knora.webapi.slice.admin.domain.service.ProjectService import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.domain.service.OntologyService +import org.knora.webapi.slice.resources.api.model.GraphDirection import org.knora.webapi.slice.resources.api.model.VersionDate import org.knora.webapi.slice.resources.repo.service.ResourcesRepo import org.knora.webapi.store.iiif.errors.SipiException @@ -177,8 +178,6 @@ final case class ResourcesResponderV2( case deleteOrEraseResourceRequestV2: DeleteOrEraseResourceRequestV2 => deleteOrEraseResourceV2(deleteOrEraseResourceRequestV2) - case graphDataGetRequest: GraphDataGetRequestV2 => getGraphDataResponseV2(graphDataGetRequest) - case resourceHistoryRequest: ResourceVersionHistoryGetRequestV2 => getResourceHistoryV2(resourceHistoryRequest) @@ -1010,11 +1009,16 @@ final case class ResourcesResponderV2( /** * Gets a graph of resources that are reachable via links to or from a given resource. * - * @param graphDataGetRequest a [[GraphDataGetRequestV2]] specifying the characteristics of the graph. * @return a [[GraphDataGetResponseV2]] representing the requested graph. */ - private def getGraphDataResponseV2(graphDataGetRequest: GraphDataGetRequestV2): Task[GraphDataGetResponseV2] = { - val excludePropertyInternal = graphDataGetRequest.excludeProperty.map(_.toOntologySchema(InternalSchema)) + def getGraphDataResponseV2( + resourceIri: IRI, + depth: Int, + dir: GraphDirection, + excludeProperty: Option[SmartIri], + requestingUser: User, + ): Task[GraphDataGetResponseV2] = { + val excludePropertyInternal = excludeProperty.map(_.toOntologySchema(InternalSchema)) /** * The internal representation of a node returned by a SPARQL query generated by the `getGraphData` template. @@ -1124,7 +1128,7 @@ final case class ResourcesResponderV2( entityCreator = node.nodeCreator, entityProject = node.nodeProject, entityPermissionLiteral = node.nodePermissions, - requestingUser = graphDataGetRequest.requestingUser, + requestingUser = requestingUser, ) .nonEmpty } @@ -1165,7 +1169,7 @@ final case class ResourcesResponderV2( entityCreator = edge.linkValueCreator, entityProject = edge.sourceNodeProject, entityPermissionLiteral = edge.linkValuePermissions, - requestingUser = graphDataGetRequest.requestingUser, + requestingUser = requestingUser, ) .nonEmpty @@ -1223,7 +1227,7 @@ final case class ResourcesResponderV2( val query = sparql.v2.txt .getGraphData( - startNodeIri = graphDataGetRequest.resourceIri, + startNodeIri = resourceIri, maybeExcludeLinkProperty = excludePropertyInternal, startNodeOnly = true, outbound = true, @@ -1235,7 +1239,7 @@ final case class ResourcesResponderV2( rows: Seq[VariableResultsRow] = response.results.bindings _ <- ZIO.when(rows.isEmpty) { - val msg = s"Resource <${graphDataGetRequest.resourceIri}> not found (it may have been deleted)" + val msg = s"Resource <$resourceIri> not found (it may have been deleted)" ZIO.fail(NotFoundException(msg)) } @@ -1257,22 +1261,22 @@ final case class ResourcesResponderV2( entityCreator = startNode.nodeCreator, entityProject = startNode.nodeProject, entityPermissionLiteral = startNode.nodePermissions, - requestingUser = graphDataGetRequest.requestingUser, + requestingUser = requestingUser, ) .isEmpty, ) { val msg = - s"User ${graphDataGetRequest.requestingUser.email} does not have permission to view resource <${graphDataGetRequest.resourceIri}>" + s"User ${requestingUser.email} does not have permission to view resource <$resourceIri>" ZIO.fail(ForbiddenException(msg)) } // Recursively get the graph containing outbound links. outboundQueryResults <- - if (graphDataGetRequest.outbound) { + if (dir.outbound) { traverseGraph( startNode = startNode, outbound = true, - depth = graphDataGetRequest.depth, + depth = depth, ) } else { ZIO.succeed(GraphQueryResults()) @@ -1280,11 +1284,11 @@ final case class ResourcesResponderV2( // Recursively get the graph containing inbound links. inboundQueryResults <- - if (graphDataGetRequest.inbound) { + if (dir.inbound) { traverseGraph( startNode = startNode, outbound = false, - depth = graphDataGetRequest.depth, + depth = depth, ) } else { ZIO.succeed(GraphQueryResults()) @@ -1440,7 +1444,7 @@ final case class ResourcesResponderV2( body = JsonLDObject( Map( JsonLDKeywords.CONTEXT -> JsonLDString("http://iiif.io/api/presentation/3/context.json"), - "id" -> JsonLDString(s"${resourceIri}/manifest"), // Is this IRI OK? + "id" -> JsonLDString(s"$resourceIri/manifest"), // Is this IRI OK? "type" -> JsonLDString("Manifest"), "label" -> JsonLDObject(Map("en" -> JsonLDArray(Seq(JsonLDString(resource.label))))), "behavior" -> JsonLDArray(Seq(JsonLDString("paged"))), diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index ac0f9a0f28..e7ded5ccaa 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -16,12 +16,10 @@ import dsp.errors.BadRequestException import dsp.valueobjects.Iri import org.knora.webapi.* import org.knora.webapi.config.AppConfig -import org.knora.webapi.config.GraphRoute import org.knora.webapi.config.Sipi import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.v2.SearchResponderV2 @@ -46,8 +44,7 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( private val jsonLdRequestParser = ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser] - private val sipiConfig: Sipi = appConfig.sipi - private val graphRouteConfig: GraphRoute = appConfig.v2.graphRoute + private val sipiConfig: Sipi = appConfig.sipi private val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") @@ -55,18 +52,11 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( private val Mapping_Iri = "mappingIri" private val GravsearchTemplate_Iri = "gravsearchTemplateIri" private val TEIHeader_XSLT_IRI = "teiHeaderXSLTIri" - private val Depth = "depth" - private val ExcludeProperty = "excludeProperty" - private val Direction = "direction" - private val Inbound = "inbound" - private val Outbound = "outbound" - private val Both = "both" def makeRoute: Route = createResource() ~ updateResourceMetadata() ~ getResourcesTei() ~ - getResourcesGraph() ~ deleteResource() ~ eraseResource() @@ -122,51 +112,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getResourcesGraph(): Route = path("v2" / "graph" / Segment) { (resIriStr: String) => - get { requestContext => - val getResourceIri = - Iri - .validateAndEscapeIri(resIriStr) - .toZIO - .orElseFail(BadRequestException(s"Invalid resource IRI: <$resIriStr>")) - val params: Map[String, String] = requestContext.request.uri.query().toMap - val getDepth: IO[BadRequestException, Int] = - ZIO - .succeed(params.get(Depth).flatMap(ValuesValidator.validateInt).getOrElse(graphRouteConfig.defaultGraphDepth)) - .filterOrFail(_ >= 1)(BadRequestException(s"$Depth must be at least 1")) - .filterOrFail(_ <= graphRouteConfig.maxGraphDepth)( - BadRequestException(s"$Depth cannot be greater than ${graphRouteConfig.maxGraphDepth}"), - ) - - val getExcludeProperty: ZIO[IriConverter, BadRequestException, Option[SmartIri]] = params - .get(ExcludeProperty) - .map(propIriStr => - ZIO - .serviceWithZIO[IriConverter](_.asSmartIri(propIriStr)) - .mapBoth(_ => BadRequestException(s"Invalid property IRI: <$propIriStr>"), Some(_)), - ) - .getOrElse(ZIO.none) - - val getInboundOutbound: IO[BadRequestException, (Boolean, Boolean)] = - params.getOrElse(Direction, Outbound) match { - case Inbound => ZIO.succeed((true, false)) - case Outbound => ZIO.succeed((false, true)) - case Both => ZIO.succeed((true, true)) - case other => ZIO.fail(BadRequestException(s"Invalid direction: $other")) - } - - val requestTask = for { - resourceIri <- getResourceIri - depth <- getDepth - excludeProperty <- getExcludeProperty - t <- getInboundOutbound - (inbound, outbound) = t - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - } yield GraphDataGetRequestV2(resourceIri, depth, inbound, outbound, excludeProperty, requestingUser) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext, RouteUtilV2.getOntologySchema(requestContext)) - } - } - private def deleteResource(): Route = path(resourcesBasePath / "delete") { post { entity(as[String]) { jsonRequest => requestContext => diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala index f4b57badc4..620f0cd200 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/DocsGenerator.scala @@ -21,6 +21,7 @@ import zio.ZLayer import zio.nio.file.Files import zio.nio.file.Path +import org.knora.webapi.config.AppConfig import org.knora.webapi.http.version.BuildInfo import org.knora.webapi.slice.admin.api.AdminApiEndpoints import org.knora.webapi.slice.admin.api.FilesEndpoints @@ -83,6 +84,7 @@ object DocsGenerator extends ZIOAppDefault { _ <- ZIO.logInfo(s"Wrote $filesWritten") } yield 0 }.provideSome[ZIOAppArgs]( + AppConfig.layer, AdminApiEndpoints.layer, ApiV2Endpoints.layer, AuthenticationEndpointsV2.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala index da98af8812..c9eeeaa2a7 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala @@ -8,6 +8,7 @@ package org.knora.webapi.slice.resources.api import zio.URLayer import zio.ZLayer +import org.knora.webapi.config.GraphRoute import org.knora.webapi.responders.v2.ResourcesResponderV2 import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.responders.v2.ValuesResponderV2 @@ -23,8 +24,8 @@ import org.knora.webapi.slice.resources.api.service.ValuesRestService object ResourcesApiModule extends URModule[ - ApiComplexV2JsonLdRequestParser & BaseEndpoints & HandlerMapper & IriConverter & KnoraResponseRenderer & - ResourcesResponderV2 & SearchResponderV2 & TapirToPekkoInterpreter & ValuesResponderV2, + ApiComplexV2JsonLdRequestParser & BaseEndpoints & GraphRoute & HandlerMapper & IriConverter & + KnoraResponseRenderer & ResourcesResponderV2 & SearchResponderV2 & TapirToPekkoInterpreter & ValuesResponderV2, ResourcesApiRoutes & ValuesEndpoints & ResourcesEndpoints, ] { self => diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index f2c695f31f..f03bfa5ec2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -15,14 +15,19 @@ import zio.ZLayer import scala.concurrent.Future import dsp.errors.RequestRejectedException +import org.knora.webapi.config.GraphRoute import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.resources.api.model.GraphDirection import org.knora.webapi.slice.resources.api.model.IriDto import org.knora.webapi.slice.resources.api.model.VersionDate -final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { +final case class ResourcesEndpoints( + private val baseEndpoints: BaseEndpoints, + private val graphConfig: GraphRoute, +) { private val base = "v2" / "resources" @@ -99,6 +104,20 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) + val getResourcesGraph = baseEndpoints.withUserEndpoint.get + .in("v2" / "graph" / path[IriDto].name("resourceIri")) + .in(ApiV2.Inputs.formatOptions) + .in( + query[Int]("depth") + .validate(Validator.min(1)) + .validate(Validator.max(graphConfig.maxGraphDepth)) + .default(graphConfig.defaultGraphDepth), + ) + .in(query[GraphDirection]("direction").default(GraphDirection.default)) + .in(query[Option[IriDto]]("excludeProperty")) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val endpoints: Seq[AnyEndpoint] = Seq( getResourcesIiifManifest, getResourcesPreview, @@ -107,6 +126,7 @@ final case class ResourcesEndpoints(private val baseEndpoints: BaseEndpoints) { getResourcesHistory, getResources, getResourcesParams, + getResourcesGraph, ).map(_.endpoint.tag("V2 Resources")) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index fe3ed54893..4f762bc818 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -18,6 +18,7 @@ final class ResourcesEndpointsHandler( val allHandlers = Seq( + SecuredEndpointHandler(resourcesEndpoints.getResourcesGraph, resourcesRestService.getResourcesGraph), SecuredEndpointHandler( resourcesEndpoints.getResourcesIiifManifest, resourcesRestService.getResourcesIiifManifest, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala index a832ceedc9..7877795310 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/Models.scala @@ -46,3 +46,25 @@ object IriDto { def from(str: String): Either[String, IriDto] = if Iri.isIri(str) then Right(IriDto(str)) else Left(s"Invalid IRI: $str") } + +enum GraphDirection(val inbound: Boolean, val outbound: Boolean) { + case Inbound extends GraphDirection(true, false) + case Outbound extends GraphDirection(false, true) + case Both extends GraphDirection(true, true) +} + +object GraphDirection { + + given TapirCodec.StringCodec[GraphDirection] = TapirCodec.stringCodec[GraphDirection]( + GraphDirection.from, + _.toString.toLowerCase, + ) + + val default: GraphDirection = GraphDirection.Outbound + + def from(str: String): Either[String, GraphDirection] = + GraphDirection.values.find(_.toString.toLowerCase == str.toLowerCase) match { + case Some(v) => Right(v) + case None => Left(s"Invalid direction: $str") + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index 272a30d085..20d3aa96de 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -16,6 +16,7 @@ import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.slice.resources.api.model.GraphDirection import org.knora.webapi.slice.resources.api.model.IriDto import org.knora.webapi.slice.resources.api.model.VersionDate @@ -107,6 +108,18 @@ final case class ResourcesRestService( private def ensureIris(values: List[String]): Task[Unit] = ZIO.foreachDiscard(values)(str => ZIO.fromEither(IriDto.from(str)).mapError(BadRequestException.apply)) + + def getResourcesGraph(user: User)( + resourceIri: IriDto, + formatOptions: FormatOptions, + depth: Int, + direction: GraphDirection, + excludeProperty: Option[IriDto], + ): Task[(RenderedResponse, MediaType)] = for { + excludeProperty <- ZIO.foreach(excludeProperty.map(_.value))(iriConverter.asSmartIri) + result <- resourcesService.getGraphDataResponseV2(resourceIri.value, depth, direction, excludeProperty, user) + response <- renderer.render(result, formatOptions) + } yield response } object ResourcesRestService { From b6272719103677c1a9b4254e9416372a0850ea47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 21:25:37 +0100 Subject: [PATCH 11/15] Migrate POST v2/resources/delete and v2/resources/erase --- .../v2/ResourcesResponderV2Spec.scala | 41 ++++--------------- .../resourcemessages/ResourceMessagesV2.scala | 3 +- .../responders/v2/ResourcesResponderV2.scala | 35 +++------------- .../webapi/routing/v2/ResourcesRouteV2.scala | 33 +-------------- .../ApiComplexV2JsonLdRequestParser.scala | 1 - .../resources/api/ResourcesEndpoints.scala | 16 ++++++++ .../api/ResourcesEndpointsHandler.scala | 2 + .../api/service/ResourcesRestService.scala | 25 ++++++++++- 8 files changed, 59 insertions(+), 97 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index 8eeb2a8f64..4c3783e965 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -1845,7 +1845,6 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => "not mark a resource as deleted with a custom delete date that is earlier than the resource's last modification date" in { val deleteDate: Instant = aThingLastModificationDate.minus(1, ChronoUnit.DAYS) - val deleteRequest = DeleteOrEraseResourceRequestV2( resourceIri = aThingIri, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, @@ -1855,12 +1854,8 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = SharedTestDataADM.anythingUser1, apiRequestID = UUID.randomUUID, ) - - appActor ! deleteRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[BadRequestException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.markResourceAsDeletedV2(deleteRequest))) + assertFailsWithA[BadRequestException](exit) } "mark a resource as deleted" in { @@ -1872,10 +1867,7 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = SharedTestDataADM.anythingUser1, apiRequestID = UUID.randomUUID, ) - - appActor ! deleteRequest - - expectMsgType[SuccessResponseV2](timeout) + val _ = UnsafeZioRun.runOrThrow(resourcesResponderV2(_.markResourceAsDeletedV2(deleteRequest))) appActor ! ResourcesGetRequestV2( resourceIris = Seq(aThingIri), @@ -1906,9 +1898,7 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => apiRequestID = UUID.randomUUID, ) - appActor ! deleteRequest - - expectMsgType[SuccessResponseV2](timeout) + val _ = UnsafeZioRun.runOrThrow(resourcesResponderV2(_.markResourceAsDeletedV2(deleteRequest))) appActor ! ResourcesGetRequestV2( resourceIris = Seq(resourceIri), @@ -2180,16 +2170,12 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => resourceIri = resourceIriToErase.get, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, maybeLastModificationDate = Some(resourceToEraseLastModificationDate), - erase = true, requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - appActor ! eraseRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[ForbiddenException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.eraseResourceV2(eraseRequest))) + assertFailsWithA[ForbiddenException](exit) } "not erase a resource if another resource has a link to it" in { @@ -2234,16 +2220,12 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => resourceIri = resourceIriToErase.get, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, maybeLastModificationDate = Some(resourceToEraseLastModificationDate), - erase = true, requestingUser = SharedTestDataADM.anythingAdminUser, apiRequestID = UUID.randomUUID, ) - appActor ! eraseRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[BadRequestException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.eraseResourceV2(eraseRequest))) + assertFailsWithA[BadRequestException](exit) // Delete the link. UnsafeZioRun.runOrThrow( @@ -2265,21 +2247,16 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => "erase a resource" in { // Erase the resource. - val eraseRequest = DeleteOrEraseResourceRequestV2( resourceIri = resourceIriToErase.get, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, maybeLastModificationDate = Some(resourceToEraseLastModificationDate), - erase = true, requestingUser = SharedTestDataADM.anythingAdminUser, apiRequestID = UUID.randomUUID, ) - - appActor ! eraseRequest - expectMsgType[SuccessResponseV2](timeout) + val _ = UnsafeZioRun.runOrThrow(resourcesResponderV2(_.eraseResourceV2(eraseRequest))) // Check that all parts of the resource were erased. - val erasedIrisToCheck: Set[SmartIri] = ( standoffTagIrisToErase.toSet + resourceIriToErase.get + diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 3a7e1ad6f4..3dd3ac3f15 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -717,10 +717,9 @@ case class DeleteOrEraseResourceRequestV2( maybeDeleteComment: Option[String] = None, maybeDeleteDate: Option[Instant] = None, maybeLastModificationDate: Option[Instant] = None, - erase: Boolean = false, requestingUser: User, apiRequestID: UUID, -) extends ResourcesResponderRequestV2 +) /** * Represents a sequence of resources read back from Knora. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 3b30ba62c8..2e7540cd94 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -175,9 +175,6 @@ final case class ResourcesResponderV2( case updateResourceMetadataRequestV2: UpdateResourceMetadataRequestV2 => updateResourceMetadataV2(updateResourceMetadataRequestV2) - case deleteOrEraseResourceRequestV2: DeleteOrEraseResourceRequestV2 => - deleteOrEraseResourceV2(deleteOrEraseResourceRequestV2) - case resourceHistoryRequest: ResourceVersionHistoryGetRequestV2 => getResourceHistoryV2(resourceHistoryRequest) @@ -338,28 +335,13 @@ final case class ResourcesResponderV2( } yield taskResult } - /** - * Either marks a resource as deleted or erases it from the triplestore, depending on the value of `erase` - * in the request message. - * - * @param deleteOrEraseResourceV2 the request message. - */ - private def deleteOrEraseResourceV2( - deleteOrEraseResourceV2: DeleteOrEraseResourceRequestV2, - ): Task[SuccessResponseV2] = - if (deleteOrEraseResourceV2.erase) { - eraseResourceV2(deleteOrEraseResourceV2) - } else { - markResourceAsDeletedV2(deleteOrEraseResourceV2) - } - /** * Marks a resource as deleted. * * @param deleteResourceV2 the request message. */ - private def markResourceAsDeletedV2(deleteResourceV2: DeleteOrEraseResourceRequestV2): Task[SuccessResponseV2] = { - def deleteTask(): Task[SuccessResponseV2] = + def markResourceAsDeletedV2(deleteResourceV2: DeleteOrEraseResourceRequestV2): Task[SuccessResponseV2] = { + val deleteTask: Task[SuccessResponseV2] = for { // Get the metadata of the resource to be updated. resourcesSeq <- getResourcePreviewV2( @@ -428,8 +410,7 @@ final case class ResourcesResponderV2( } } yield SuccessResponseV2("Resource marked as deleted") - ZIO.when(deleteResourceV2.erase)(ZIO.fail(AssertionException(s"Request message has erase == true"))) *> - IriLocker.runWithIriLock(deleteResourceV2.apiRequestID, deleteResourceV2.resourceIri, deleteTask()) + IriLocker.runWithIriLock(deleteResourceV2.apiRequestID, deleteResourceV2.resourceIri, deleteTask) } /** @@ -437,8 +418,8 @@ final case class ResourcesResponderV2( * * @param eraseResourceV2 the request message. */ - private def eraseResourceV2(eraseResourceV2: DeleteOrEraseResourceRequestV2): Task[SuccessResponseV2] = { - def eraseTask: Task[SuccessResponseV2] = + def eraseResourceV2(eraseResourceV2: DeleteOrEraseResourceRequestV2): Task[SuccessResponseV2] = { + val eraseTask: Task[SuccessResponseV2] = for { // Get the metadata of the resource to be updated. resourcesSeq <- getResourcePreviewV2( @@ -498,11 +479,7 @@ final case class ResourcesResponderV2( ) .whenZIO(iriService.checkIriExists(resourceSmartIri.toString)) } yield SuccessResponseV2("Resource erased") - - for { - _ <- ZIO.when(!eraseResourceV2.erase)(ZIO.fail(AssertionException(s"Request message has erase == false"))) - taskResult <- IriLocker.runWithIriLock(eraseResourceV2.apiRequestID, eraseResourceV2.resourceIri, eraseTask) - } yield taskResult + IriLocker.runWithIriLock(eraseResourceV2.apiRequestID, eraseResourceV2.resourceIri, eraseTask) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index e7ded5ccaa..40336afc66 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -56,9 +56,7 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( def makeRoute: Route = createResource() ~ updateResourceMetadata() ~ - getResourcesTei() ~ - deleteResource() ~ - eraseResource() + getResourcesTei() private def createResource(): Route = path(resourcesBasePath) { post { @@ -112,35 +110,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def deleteResource(): Route = path(resourcesBasePath / "delete") { - post { - entity(as[String]) { jsonRequest => requestContext => - val requestTask = for { - apiRequestId <- RouteUtilZ.randomUuid() - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - msg <- jsonLdRequestParser(_.deleteOrEraseResourceRequestV2(jsonRequest, requestingUser, apiRequestId)) - .mapError(BadRequestException.apply) - } yield msg - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - } - - private def eraseResource(): Route = path(resourcesBasePath / "erase") { - post { - entity(as[String]) { jsonRequest => requestContext => - val requestTask = for { - apiRequestId <- RouteUtilZ.randomUuid() - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - requestMessage <- - jsonLdRequestParser(_.deleteOrEraseResourceRequestV2(jsonRequest, requestingUser, apiRequestId)) - .mapError(BadRequestException.apply) - } yield requestMessage.copy(erase = true) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - } - /** * Gets the Iri of the property that represents the text of the resource. * diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala index 0c5adc06fd..fecd4cc1db 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala @@ -146,7 +146,6 @@ final case class ApiComplexV2JsonLdRequestParser( deleteComment, deleteDate, lastModificationDate, - false, requestingUser, uuid, ) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index f03bfa5ec2..237d8e476f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -118,6 +118,20 @@ final case class ResourcesEndpoints( .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) + val postResourcesErase = baseEndpoints.withUserEndpoint.post + .in(base / "erase") + .in(ApiV2.Inputs.formatOptions) + .in(stringJsonBody) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + + val postResourcesDelete = baseEndpoints.withUserEndpoint.post + .in(base / "delete") + .in(ApiV2.Inputs.formatOptions) + .in(stringJsonBody) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val endpoints: Seq[AnyEndpoint] = Seq( getResourcesIiifManifest, getResourcesPreview, @@ -127,6 +141,8 @@ final case class ResourcesEndpoints( getResources, getResourcesParams, getResourcesGraph, + postResourcesErase, + postResourcesDelete, ).map(_.endpoint.tag("V2 Resources")) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index 4f762bc818..862fd18bb6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -41,6 +41,8 @@ final class ResourcesEndpointsHandler( resourcesRestService.searchResourcesByProjectAndClass, ), SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), + SecuredEndpointHandler(resourcesEndpoints.postResourcesErase, resourcesRestService.eraseResource), + SecuredEndpointHandler(resourcesEndpoints.postResourcesDelete, resourcesRestService.deleteResource), ).map(mapper.mapSecuredEndpointHandler(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index 20d3aa96de..d6159fd0d0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -12,6 +12,7 @@ import org.knora.webapi.responders.v2.ResourcesResponderV2 import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse @@ -24,7 +25,8 @@ final case class ResourcesRestService( private val resourcesService: ResourcesResponderV2, private val searchService: SearchResponderV2, private val iriConverter: IriConverter, - renderer: KnoraResponseRenderer, + private val requestParser: ApiComplexV2JsonLdRequestParser, + private val renderer: KnoraResponseRenderer, ) { def getResourcesIiifManifest(user: User)( @@ -120,6 +122,27 @@ final case class ResourcesRestService( result <- resourcesService.getGraphDataResponseV2(resourceIri.value, depth, direction, excludeProperty, user) response <- renderer.render(result, formatOptions) } yield response + + def eraseResource(user: User)(formatOptions: FormatOptions, jsonLd: String): Task[(RenderedResponse, MediaType)] = + for { + uuid <- Random.nextUUID + eraseRequest <- requestParser + .deleteOrEraseResourceRequestV2(jsonLd, user, uuid) + .mapError(BadRequestException.apply) + result <- resourcesService.eraseResourceV2(eraseRequest) + response <- renderer.render(result, formatOptions) + } yield response + + def deleteResource(user: User)(formatOptions: FormatOptions, jsonLd: String): Task[(RenderedResponse, MediaType)] = + for { + uuid <- Random.nextUUID + eraseRequest <- requestParser + .deleteOrEraseResourceRequestV2(jsonLd, user, uuid) + .mapError(BadRequestException.apply) + result <- resourcesService.markResourceAsDeletedV2(eraseRequest) + response <- renderer.render(result, formatOptions) + } yield response + } object ResourcesRestService { From b1fe1152aa75b86f48017875085850bce59166fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 21:55:10 +0100 Subject: [PATCH 12/15] Migrate POST v2/resources/delete and v2/resources/erase --- .../v2/ResourcesResponderV2Spec.scala | 64 +++++-------------- .../resourcemessages/ResourceMessagesV2.scala | 2 +- .../responders/v2/ResourcesResponderV2.scala | 5 +- .../webapi/routing/v2/ResourcesRouteV2.scala | 20 +----- .../resources/api/ResourcesEndpoints.scala | 8 +++ .../api/ResourcesEndpointsHandler.scala | 1 + .../api/service/ResourcesRestService.scala | 11 ++++ 7 files changed, 38 insertions(+), 73 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index 4c3783e965..6dba6e2a24 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -5,7 +5,6 @@ package org.knora.webapi.responders.v2 -import org.apache.pekko.actor.Status.Failure import org.apache.pekko.testkit.ImplicitSender import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input @@ -1689,12 +1688,8 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = incunabulaUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[ForbiddenException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) + assertFailsWithA[ForbiddenException](exit) } "not update a resource's metadata if the user does not supply the correct resource class" in { @@ -1705,12 +1700,8 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[BadRequestException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) + assertFailsWithA[BadRequestException](exit) } "update a resource's metadata when it doesn't have a knora-base:lastModificationDate" in { @@ -1726,13 +1717,9 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgType[UpdateResourceMetadataResponseV2](timeout) + val _ = UnsafeZioRun.runOrThrow(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) // Get the resource from the triplestore and check it. - val outputResource: ReadResourceV2 = getResource(aThingIri) assert(outputResource.label == newLabel) assert( @@ -1752,12 +1739,8 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[EditConflictException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) + assertFailsWithA[EditConflictException](exit) } "not update a resource's metadata if the wrong knora-base:lastModificationDate is submitted" in { @@ -1769,12 +1752,8 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[EditConflictException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) + assertFailsWithA[EditConflictException](exit) } "update a resource's metadata when it has a knora-base:lastModificationDate" in { @@ -1788,13 +1767,9 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgType[UpdateResourceMetadataResponseV2](timeout) + val _ = UnsafeZioRun.runOrThrow(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) // Get the resource from the triplestore and check it. - val outputResource: ReadResourceV2 = getResource(aThingIri) assert(outputResource.label == newLabel) val updatedLastModificationDate = outputResource.lastModificationDate.get @@ -1811,12 +1786,8 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgPF(timeout) { case msg: Failure => - msg.cause.isInstanceOf[BadRequestException] should ===(true) - } + val exit = UnsafeZioRun.run(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) + assertFailsWithA[BadRequestException](exit) } "update a resource's knora-base:lastModificationDate" in { @@ -1830,10 +1801,7 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - appActor ! updateRequest - - expectMsgType[UpdateResourceMetadataResponseV2](timeout) + val _ = UnsafeZioRun.runOrThrow(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) // Get the resource from the triplestore and check it. @@ -2537,16 +2505,14 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => "update resource's metadata to test update resource metadata event" in { val resourceIri = "http://rdfh.ch/0001/thing_with_BCE_date2" - appActor ! UpdateResourceMetadataRequestV2( + val updateRequest = UpdateResourceMetadataRequestV2( resourceIri = resourceIri, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, maybeLabel = Some("a new label"), requestingUser = anythingUserProfile, apiRequestID = UUID.randomUUID, ) - - expectMsgType[UpdateResourceMetadataResponseV2](timeout) - + val _ = UnsafeZioRun.runOrThrow(resourcesResponderV2(_.updateResourceMetadataV2(updateRequest))) val events = UnsafeZioRun.runOrThrow( resourcesResponderV2(_.getResourceHistoryEvents(resourceIri, anythingUserProfile).map(_.historyEvents)), ) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 3dd3ac3f15..126e0651c8 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -615,7 +615,7 @@ case class UpdateResourceMetadataRequestV2( maybeNewModificationDate: Option[Instant] = None, requestingUser: User, apiRequestID: UUID, -) extends ResourcesResponderRequestV2 +) /** * Represents a response after updating a resource's metadata. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 2e7540cd94..e873865425 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -172,9 +172,6 @@ final case class ResourcesResponderV2( case createResourceRequestV2: CreateResourceRequestV2 => createHandler(createResourceRequestV2) - case updateResourceMetadataRequestV2: UpdateResourceMetadataRequestV2 => - updateResourceMetadataV2(updateResourceMetadataRequestV2) - case resourceHistoryRequest: ResourceVersionHistoryGetRequestV2 => getResourceHistoryV2(resourceHistoryRequest) @@ -222,7 +219,7 @@ final case class ResourcesResponderV2( * @param updateResourceMetadataRequestV2 the update request. * @return a [[UpdateResourceMetadataResponseV2]]. */ - private def updateResourceMetadataV2( + def updateResourceMetadataV2( updateResourceMetadataRequestV2: UpdateResourceMetadataRequestV2, ): Task[UpdateResourceMetadataResponseV2] = { def makeTaskFuture: Task[UpdateResourceMetadataResponseV2] = diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 40336afc66..816654d9f7 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -53,10 +53,7 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( private val GravsearchTemplate_Iri = "gravsearchTemplateIri" private val TEIHeader_XSLT_IRI = "teiHeaderXSLTIri" - def makeRoute: Route = - createResource() ~ - updateResourceMetadata() ~ - getResourcesTei() + def makeRoute: Route = createResource() ~ getResourcesTei() private def createResource(): Route = path(resourcesBasePath) { post { @@ -75,21 +72,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def updateResourceMetadata(): Route = path(resourcesBasePath) { - put { - entity(as[String]) { jsonRequest => requestContext => - val requestMessageFuture = for { - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - apiRequestId <- RouteUtilZ.randomUuid() - requestMessage <- - jsonLdRequestParser(_.updateResourceMetadataRequestV2(jsonRequest, requestingUser, apiRequestId)) - .mapError(BadRequestException.apply) - } yield requestMessage - RouteUtilV2.runRdfRouteZ(requestMessageFuture, requestContext) - } - } - } - private def getResourcesTei(): Route = path("v2" / "tei" / Segment) { (resIri: String) => get { requestContext => val params: Map[String, String] = requestContext.request.uri.query().toMap diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index 237d8e476f..6c18424e52 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -132,6 +132,13 @@ final case class ResourcesEndpoints( .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) + val putResources = baseEndpoints.withUserEndpoint.put + .in(base) + .in(ApiV2.Inputs.formatOptions) + .in(stringJsonBody) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val endpoints: Seq[AnyEndpoint] = Seq( getResourcesIiifManifest, getResourcesPreview, @@ -143,6 +150,7 @@ final case class ResourcesEndpoints( getResourcesGraph, postResourcesErase, postResourcesDelete, + putResources, ).map(_.endpoint.tag("V2 Resources")) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index 862fd18bb6..936ec4c505 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -43,6 +43,7 @@ final class ResourcesEndpointsHandler( SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), SecuredEndpointHandler(resourcesEndpoints.postResourcesErase, resourcesRestService.eraseResource), SecuredEndpointHandler(resourcesEndpoints.postResourcesDelete, resourcesRestService.deleteResource), + SecuredEndpointHandler(resourcesEndpoints.putResources, resourcesRestService.updateResourceMetadata), ).map(mapper.mapSecuredEndpointHandler(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index d6159fd0d0..a065712d30 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -143,6 +143,17 @@ final case class ResourcesRestService( response <- renderer.render(result, formatOptions) } yield response + def updateResourceMetadata( + user: User, + )(formatOptions: FormatOptions, jsonLd: String): Task[(RenderedResponse, MediaType)] = + for { + uuid <- Random.nextUUID + eraseRequest <- requestParser + .updateResourceMetadataRequestV2(jsonLd, user, uuid) + .mapError(BadRequestException.apply) + result <- resourcesService.updateResourceMetadataV2(eraseRequest) + response <- renderer.render(result, formatOptions) + } yield response } object ResourcesRestService { From 6669c2907f15420c60c3c5621a160a077d5a0647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 27 Feb 2025 22:26:20 +0100 Subject: [PATCH 13/15] Migrate GET v2/tei/:resourceIri --- .../v2/ResourcesResponderV2Spec.scala | 72 ++++++------ .../resourcemessages/ResourceMessagesV2.scala | 19 ---- .../responders/v2/ResourcesResponderV2.scala | 18 +-- .../knora/webapi/routing/RouteUtilV2.scala | 23 ---- .../webapi/routing/v2/ResourcesRouteV2.scala | 106 +----------------- .../resources/api/ResourcesEndpoints.scala | 10 ++ .../api/ResourcesEndpointsHandler.scala | 1 + .../api/service/ResourcesRestService.scala | 15 +++ 8 files changed, 62 insertions(+), 202 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index 6dba6e2a24..e33c5de660 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -666,51 +666,43 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { self => } "return a resource of type thing with text as TEI/XML" in { - - appActor ! ResourceTEIGetRequestV2( - resourceIri = "http://rdfh.ch/0001/thing_with_richtext_with_markup", - textProperty = "http://www.knora.org/ontology/0001/anything#hasRichtext".toSmartIri, - mappingIri = None, - gravsearchTemplateIri = None, - headerXSLTIri = None, - requestingUser = anythingUserProfile, + val response = UnsafeZioRun.runOrThrow( + resourcesResponderV2( + _.getResourceAsTeiV2( + resourceIri = "http://rdfh.ch/0001/thing_with_richtext_with_markup", + textProperty = "http://www.knora.org/ontology/0001/anything#hasRichtext".toSmartIri, + mappingIri = None, + gravsearchTemplateIri = None, + headerXSLTIri = None, + requestingUser = anythingUserProfile, + ), + ), ) - - expectMsgPF(timeout) { case response: ResourceTEIGetResponseV2 => - val expectedBody = - """

This is a test that contains marked up elements. This is interesting text in italics. This is boring text in italics.

""".stripMargin - - // Compare the original XML with the regenerated XML. - val xmlDiff: Diff = - DiffBuilder.compare(Input.fromString(response.body.toXML)).withTest(Input.fromString(expectedBody)).build() - - xmlDiff.hasDifferences should be(false) - } - + val expectedBody = + """

This is a test that contains marked up elements. This is interesting text in italics. This is boring text in italics.

""".stripMargin + val xmlDiff: Diff = // Compare the original XML with the regenerated XML. + DiffBuilder.compare(Input.fromString(response.body.toXML)).withTest(Input.fromString(expectedBody)).build() + xmlDiff.hasDifferences should be(false) } "return a resource of type Something with text with standoff as TEI/XML" in { - - appActor ! ResourceTEIGetRequestV2( - resourceIri = "http://rdfh.ch/0001/qN1igiDRSAemBBktbRHn6g", - textProperty = "http://www.knora.org/ontology/0001/anything#hasRichtext".toSmartIri, - mappingIri = None, - gravsearchTemplateIri = None, - headerXSLTIri = None, - requestingUser = anythingUserProfile, + val response = UnsafeZioRun.runOrThrow( + resourcesResponderV2( + _.getResourceAsTeiV2( + resourceIri = "http://rdfh.ch/0001/qN1igiDRSAemBBktbRHn6g", + textProperty = "http://www.knora.org/ontology/0001/anything#hasRichtext".toSmartIri, + mappingIri = None, + gravsearchTemplateIri = None, + headerXSLTIri = None, + requestingUser = anythingUserProfile, + ), + ), ) - - expectMsgPF(timeout) { case response: ResourceTEIGetResponseV2 => - val expectedBody = - """

Something with a lot of different markup. And more markup.

""".stripMargin - - // Compare the original XML with the regenerated XML. - val xmlDiff: Diff = - DiffBuilder.compare(Input.fromString(response.body.toXML)).withTest(Input.fromString(expectedBody)).build() - - xmlDiff.hasDifferences should be(false) - } - + val expectedBody = + """

Something with a lot of different markup. And more markup.

""".stripMargin + val xmlDiff: Diff = // Compare the original XML with the regenerated XML. + DiffBuilder.compare(Input.fromString(response.body.toXML)).withTest(Input.fromString(expectedBody)).build() + xmlDiff.hasDifferences should be(false) } "return a past version of a resource" in { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 126e0651c8..3d9b845512 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -176,25 +176,6 @@ case class ResourceVersionHistoryResponseV2(history: Seq[ResourceHistoryEntry]) } } -/** - * Requests a resource as TEI/XML. A successful response will be a [[ResourceTEIGetResponseV2]]. - * - * @param resourceIri the IRI of the resource to be returned in TEI/XML. - * @param textProperty the property representing the text (to be converted to the body of a TEI document). - * @param mappingIri the IRI of the mapping to be used to convert from standoff to TEI/XML, if any. Otherwise the standard mapping is assumed. - * @param gravsearchTemplateIri the gravsearch template to query the metadata for the TEI header, if provided. - * @param headerXSLTIri the IRI of the XSL transformation to convert the resource's metadata to the TEI header. - * @param requestingUser the user making the request. - */ -case class ResourceTEIGetRequestV2( - resourceIri: IRI, - textProperty: SmartIri, - mappingIri: Option[IRI], - gravsearchTemplateIri: Option[IRI], - headerXSLTIri: Option[IRI], - requestingUser: User, -) extends ResourcesResponderRequestV2 - /** * Represents a Knora resource as TEI/XML. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index e873865425..c6c5df6455 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -152,22 +152,6 @@ final case class ResourcesResponderV2( requestingUser, ) => getResourcePreviewV2(resIris, withDeletedResource, targetSchema, requestingUser) - case ResourceTEIGetRequestV2( - resIri, - textProperty, - mappingIri, - gravsearchTemplateIri, - headerXSLTIri, - requestingUser, - ) => - getResourceAsTeiV2( - resIri, - textProperty, - mappingIri, - gravsearchTemplateIri, - headerXSLTIri, - requestingUser, - ) case createResourceRequestV2: CreateResourceRequestV2 => createHandler(createResourceRequestV2) @@ -782,7 +766,7 @@ final case class ResourcesResponderV2( * @param requestingUser the user making the request. * @return a [[ResourceTEIGetResponseV2]]. */ - private def getResourceAsTeiV2( + def getResourceAsTeiV2( resourceIri: IRI, textProperty: SmartIri, mappingIri: Option[IRI], diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala index 0d482a43f4..33fcfad34a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala @@ -22,7 +22,6 @@ import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.rdf.RdfFormat import org.knora.webapi.messages.v2.responder.KnoraResponseV2 -import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceTEIGetResponseV2 import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.resourceinfo.domain.IriConverter @@ -175,28 +174,6 @@ object RouteUtilV2 { routeResult <- ZIO.fromFuture(_ => requestContext.complete(response)) } yield routeResult) - /** - * Sends a message to a responder and completes the HTTP request by returning the response as TEI/XML. - * - * @param requestTask a [[Task]] containing a [[KnoraRequestV2]] message that should be sent to the responder manager. - * @param requestContext the pekko-http [[RequestContext]]. - * - * @return a [[Future]] containing a [[RouteResult]]. - */ - def runTEIXMLRoute[R]( - requestTask: ZIO[R, Throwable, KnoraRequestV2], - requestContext: RequestContext, - )(implicit runtime: Runtime[R & MessageRelay]): Future[RouteResult] = - UnsafeZioRun.runToFuture { - for { - requestMessage <- requestTask - teiResponse <- ZIO.serviceWithZIO[MessageRelay](_.ask[ResourceTEIGetResponseV2](requestMessage)) - contentType = MediaTypes.`application/xml`.toContentType(HttpCharsets.`UTF-8`) - response = HttpResponse(StatusCodes.OK, entity = HttpEntity(contentType, teiResponse.toXML)) - completed <- ZIO.fromFuture(_ => requestContext.complete(response)) - } yield completed - } - private def extractMediaTypeFromHeaderItem( headerValueItem: String, headerValue: String, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 816654d9f7..97965131c4 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -13,12 +13,10 @@ import zio.* import zio.ZIO import dsp.errors.BadRequestException -import dsp.valueobjects.Iri import org.knora.webapi.* import org.knora.webapi.config.AppConfig import org.knora.webapi.config.Sipi import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* @@ -42,18 +40,11 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( ], ) extends LazyLogging { - private val jsonLdRequestParser = ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser] - - private val sipiConfig: Sipi = appConfig.sipi - + private val jsonLdRequestParser = ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser] + private val sipiConfig: Sipi = appConfig.sipi private val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") - private val Text_Property = "textProperty" - private val Mapping_Iri = "mappingIri" - private val GravsearchTemplate_Iri = "gravsearchTemplateIri" - private val TEIHeader_XSLT_IRI = "teiHeaderXSLTIri" - - def makeRoute: Route = createResource() ~ getResourcesTei() + def makeRoute: Route = createResource() private def createResource(): Route = path(resourcesBasePath) { post { @@ -72,97 +63,6 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( } } - private def getResourcesTei(): Route = path("v2" / "tei" / Segment) { (resIri: String) => - get { requestContext => - val params: Map[String, String] = requestContext.request.uri.query().toMap - val getResourceIri = - Iri - .validateAndEscapeIri(resIri) - .toZIO - .orElseFail(BadRequestException(s"Invalid resource IRI: <$resIri>")) - val requestTask = for { - resourceIri <- getResourceIri - mappingIri <- getMappingIriFromParams(params) - textProperty <- getTextPropertyFromParams(params) - gravsearchTemplateIri <- getGravsearchTemplateIriFromParams(params) - headerXSLTIri <- getHeaderXSLTIriFromParams(params) - user <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - } yield ResourceTEIGetRequestV2(resourceIri, textProperty, mappingIri, gravsearchTemplateIri, headerXSLTIri, user) - RouteUtilV2.runTEIXMLRoute(requestTask, requestContext) - } - } - - /** - * Gets the Iri of the property that represents the text of the resource. - * - * @param params the GET parameters. - * @return the internal resource class, if any. - */ - private def getTextPropertyFromParams(params: Map[String, String]): ZIO[IriConverter, Throwable, SmartIri] = - ZIO - .fromOption(params.get(Text_Property)) - .orElseFail(BadRequestException(s"param $Text_Property not set")) - .flatMap { textPropIriStr => - ZIO - .serviceWithZIO[IriConverter](_.asSmartIri(textPropIriStr)) - .orElseFail(BadRequestException(s"Invalid property IRI: <$textPropIriStr>")) - .filterOrFail(_.isKnoraApiV2EntityIri)( - BadRequestException(s"<$textPropIriStr> is not a valid knora-api property IRI"), - ) - .mapAttempt(_.toOntologySchema(InternalSchema)) - } - - /** - * Gets the Iri of the mapping to be used to convert standoff to XML. - * - * @param params the GET parameters. - * @return the internal resource class, if any. - */ - private def getMappingIriFromParams(params: Map[String, String]): IO[BadRequestException, Option[IRI]] = - params - .get(Mapping_Iri) - .map { mapping => - Iri - .validateAndEscapeIri(mapping) - .toZIO - .mapBoth(_ => BadRequestException(s"Invalid mapping IRI: <$mapping>"), Some(_)) - } - .getOrElse(ZIO.none) - - /** - * Gets the Iri of Gravsearch template to be used to query for the resource's metadata. - * - * @param params the GET parameters. - * @return the internal resource class, if any. - */ - private def getGravsearchTemplateIriFromParams(params: Map[String, String]): IO[BadRequestException, Option[IRI]] = - params - .get(GravsearchTemplate_Iri) - .map { gravsearch => - Iri - .validateAndEscapeIri(gravsearch) - .toZIO - .mapBoth(_ => BadRequestException(s"Invalid template IRI: <$gravsearch>"), Some(_)) - } - .getOrElse(ZIO.none) - - /** - * Gets the Iri of the XSL transformation to be used to convert the TEI header's metadata. - * - * @param params the GET parameters. - * @return the internal resource class, if any. - */ - private def getHeaderXSLTIriFromParams(params: Map[String, String]): IO[BadRequestException, Option[IRI]] = - params - .get(TEIHeader_XSLT_IRI) - .map { xslt => - Iri - .validateAndEscapeIri(xslt) - .toZIO - .mapBoth(_ => BadRequestException(s"Invalid XSLT IRI: <$xslt>"), Some(_)) - } - .getOrElse(ZIO.none) - /** * Checks if the MIME types of the given values are allowed by the configuration * diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index 6c18424e52..6db67afc78 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -125,6 +125,15 @@ final case class ResourcesEndpoints( .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) + val getResourcesTei = baseEndpoints.withUserEndpoint.get + .in("v2" / "tei" / path[IriDto].name("resourceIri")) + .in(query[Option[IriDto]]("mappingIri")) + .in(query[IriDto]("textProperty")) + .in(query[Option[IriDto]]("gravsearchTemplateIri")) + .in(query[Option[IriDto]]("headerXSLTIri")) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val postResourcesDelete = baseEndpoints.withUserEndpoint.post .in(base / "delete") .in(ApiV2.Inputs.formatOptions) @@ -148,6 +157,7 @@ final case class ResourcesEndpoints( getResources, getResourcesParams, getResourcesGraph, + getResourcesTei, postResourcesErase, postResourcesDelete, putResources, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index 936ec4c505..719871a1dc 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -41,6 +41,7 @@ final class ResourcesEndpointsHandler( resourcesRestService.searchResourcesByProjectAndClass, ), SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), + SecuredEndpointHandler(resourcesEndpoints.getResourcesTei, resourcesRestService.getResourceAsTeiV2), SecuredEndpointHandler(resourcesEndpoints.postResourcesErase, resourcesRestService.eraseResource), SecuredEndpointHandler(resourcesEndpoints.postResourcesDelete, resourcesRestService.deleteResource), SecuredEndpointHandler(resourcesEndpoints.putResources, resourcesRestService.updateResourceMetadata), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index a065712d30..a49a55e513 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -123,6 +123,21 @@ final case class ResourcesRestService( response <- renderer.render(result, formatOptions) } yield response + def getResourceAsTeiV2(user: User)( + resourceIri: IriDto, + mappingIri: Option[IriDto], + textProperty: IriDto, + gravsearchTemplateIri: Option[IriDto], + headerXSLTIri: Option[IriDto], + ) = for { + textProp <- iriConverter.asSmartIri(textProperty.value) + resource = resourceIri.value + mapping = mappingIri.map(_.value) + gravsearchTemplate = gravsearchTemplateIri.map(_.value) + headerXslt = headerXSLTIri.map(_.value) + result <- resourcesService.getResourceAsTeiV2(resource, textProp, mapping, gravsearchTemplate, headerXslt, user) + } yield (result.toXML, MediaType.ApplicationXml) + def eraseResource(user: User)(formatOptions: FormatOptions, jsonLd: String): Task[(RenderedResponse, MediaType)] = for { uuid <- Random.nextUUID From 5fd770204ae2c6941e00d4613d16c45253509e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 28 Feb 2025 10:18:24 +0100 Subject: [PATCH 14/15] Migrate POST v2/resources --- .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 56 +++----- .../webapi/e2e/v2/SearchRouteV2R2RSpec.scala | 133 ++++++++---------- .../v2/ontology/CardinalitiesV2E2ESpec.scala | 2 +- .../responders/v2/ValuesResponderV2Spec.scala | 52 ++++--- webapi/src/main/scala/dsp/errors/Errors.scala | 4 +- .../http/status/ApiStatusCodesADM.scala | 43 ------ .../webapi/http/status/ApiStatusCodesZ.scala | 49 ------- .../responders/v2/ResourcesResponderV2.scala | 3 - .../org/knora/webapi/routing/ApiRoutes.scala | 4 +- .../webapi/routing/v2/ResourcesRouteV2.scala | 105 -------------- .../ApiComplexV2JsonLdRequestParser.scala | 61 ++++++-- .../slice/common/api/BaseEndpoints.scala | 3 + .../resources/api/ResourcesEndpoints.scala | 22 ++- .../api/ResourcesEndpointsHandler.scala | 1 + .../api/service/ResourcesRestService.scala | 8 ++ 15 files changed, 193 insertions(+), 353 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesADM.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala index 0d0149b5e2..8ab9ebbc23 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala @@ -7,6 +7,7 @@ package org.knora.webapi.e2e.v2 import org.apache.pekko import spray.json.JsString +import zio.ZIO import java.net.URLEncoder import java.time.Instant @@ -31,10 +32,11 @@ import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.util.rdf.* import org.knora.webapi.models.* +import org.knora.webapi.routing.UnsafeZioRun import org.knora.webapi.routing.v2.OntologiesRouteV2 -import org.knora.webapi.routing.v2.ResourcesRouteV2 import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.testservices.TestClientService import org.knora.webapi.util.* import pekko.http.scaladsl.model.* @@ -61,8 +63,6 @@ class OntologyV2R2RSpec extends R2RSpec { private val ontologiesPath = DSPApiDirectives.handleErrors(appConfig)(OntologiesRouteV2().makeRoute) - private val resourcesPath = - DSPApiDirectives.handleErrors(appConfig)(ResourcesRouteV2(appConfig).makeRoute) implicit val ec: ExecutionContextExecutor = system.dispatcher @@ -2820,23 +2820,16 @@ class OntologyV2R2RSpec extends R2RSpec { | "freetest" : "${SharedOntologyTestDataADM.FREETEST_ONTOLOGY_IRI_LocalHost}#" | } |}""".stripMargin - - Post( - "/v2/resources", - HttpEntity(RdfMediaTypes.`application/ld+json`, createResourceWithValues), - ) ~> addCredentials(BasicHttpCredentials(anythingUsername, password)) ~> resourcesPath ~> check { - val responseStr = responseAs[String] - assert(status == StatusCodes.OK, responseStr) - - val responseJsonDoc = JsonLDUtil.parseJsonLD(responseStr) - val validationFun: (String, => Nothing) => String = (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) - val resourceIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) - assert(resourceIri.toSmartIri.isKnoraDataIri) - - val responseJsonDoc2 = JsonLDUtil.parseJsonLD(responseStr) - val label = responseJsonDoc2.body.value.get(OntologyConstants.Rdfs.Label).get - assert(label == JsonLDString(resourceLabel)) - } + val _ = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[TestClientService]( + _.checkResponseOK( + Post( + appConfig.knoraApi.internalKnoraApiBaseUrl + "/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, createResourceWithValues), + ) ~> addCredentials(BasicHttpCredentials(anythingUsername, password)), + ), + ), + ) // payload to test cardinality can't be deleted val cardinalityCantBeDeletedPayload = AddCardinalitiesRequest.make( @@ -3098,19 +3091,16 @@ class OntologyV2R2RSpec extends R2RSpec { | "freetest" : "${SharedOntologyTestDataADM.FREETEST_ONTOLOGY_IRI_LocalHost}#" | } |}""".stripMargin - - Post( - "/v2/resources", - HttpEntity(RdfMediaTypes.`application/ld+json`, createResourceWithValues), - ) ~> addCredentials(BasicHttpCredentials(anythingUsername, password)) ~> resourcesPath ~> check { - val responseStr = responseAs[String] - assert(status == StatusCodes.OK, responseStr) - val responseJsonDoc = JsonLDUtil.parseJsonLD(responseStr) - val validationFun: (String, => Nothing) => String = (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) - val resourceIri: IRI = - responseJsonDoc.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) - assert(resourceIri.toSmartIri.isKnoraDataIri) - } + val _ = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[TestClientService]( + _.checkResponseOK( + Post( + appConfig.knoraApi.internalKnoraApiBaseUrl + "/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, createResourceWithValues), + ) ~> addCredentials(BasicHttpCredentials(anythingUsername, password)), + ), + ), + ) // payload to ask if cardinality can be removed from TestClassTwo val cardinalityCanBeDeletedPayload = AddCardinalitiesRequest diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index b13acab36c..23506f4188 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -30,7 +30,6 @@ import org.knora.webapi.messages.util.rdf.JsonLDKeywords import org.knora.webapi.messages.util.rdf.JsonLDUtil import org.knora.webapi.messages.util.search.SparqlQueryConstants import org.knora.webapi.routing.UnsafeZioRun -import org.knora.webapi.routing.v2.ResourcesRouteV2 import org.knora.webapi.routing.v2.StandoffRouteV2 import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.slice.search.api.SearchApiRoutes @@ -54,8 +53,6 @@ class SearchRouteV2R2RSpec extends R2RSpec { .runOrThrow(ZIO.serviceWith[SearchApiRoutes](_.routes)) .reduce(_ ~ _) - private val resourcePath = - DSPApiDirectives.handleErrors(appConfig)(ResourcesRouteV2(appConfig).makeRoute) private val standoffPath = DSPApiDirectives.handleErrors(appConfig)(StandoffRouteV2().makeRoute) @@ -7345,19 +7342,25 @@ class SearchRouteV2R2RSpec extends R2RSpec { | } |}""".stripMargin - Post("/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials( - BasicHttpCredentials(anythingUserEmail, password), - ) ~> resourcePath ~> check { - val resourceCreateResponseStr = responseAs[String] - assert(status == StatusCodes.OK, resourceCreateResponseStr) - val resourceCreateResponseAsJsonLD: JsonLDDocument = JsonLDUtil.parseJsonLD(resourceCreateResponseStr) - val validationFun: (String, => Nothing) => String = - (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) - val resourceIri: IRI = - resourceCreateResponseAsJsonLD.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) - assert(resourceIri.toSmartIri.isKnoraDataIri) - hamletResourceIri.set(resourceIri) - } + val resourceCreateResponseStr = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[TestClientService]( + _.getResponseString( + Post( + appConfig.knoraApi.internalKnoraApiBaseUrl + "/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity), + ) ~> addCredentials( + BasicHttpCredentials(anythingUserEmail, password), + ), + ), + ), + ) + val resourceCreateResponseAsJsonLD: JsonLDDocument = JsonLDUtil.parseJsonLD(resourceCreateResponseStr) + val validationFun: (String, => Nothing) => String = + (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) + val resourceIri: IRI = + resourceCreateResponseAsJsonLD.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) + assert(resourceIri.toSmartIri.isKnoraDataIri) + hamletResourceIri.set(resourceIri) } "search for the large text and its markup and receive it as XML, and check that it matches the original XML" ignore { // depends on previous test @@ -7399,7 +7402,6 @@ class SearchRouteV2R2RSpec extends R2RSpec { "find a resource with two different incoming links" in { // Create the target resource. - val targetResource: String = """{ | "@type" : "anything:BlueThing", @@ -7415,20 +7417,16 @@ class SearchRouteV2R2RSpec extends R2RSpec { | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" | } |}""".stripMargin - - val targetResourceIri: IRI = - Post(s"/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, targetResource)) ~> addCredentials( - BasicHttpCredentials(anythingUserEmail, password), - ) ~> resourcePath ~> check { - val createTargetResourceResponseStr = responseAs[String] - assert(response.status == StatusCodes.OK, createTargetResourceResponseStr) - val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) - val validationFun: (String, => Nothing) => String = - (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) - responseJsonDoc.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) - } - - assert(targetResourceIri.toSmartIri.isKnoraDataIri) + val targetResourceIri = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[TestClientService]( + _.getResponseJsonLD( + Post( + appConfig.knoraApi.internalKnoraApiBaseUrl + s"/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, targetResource), + ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)), + ).map(_.body.getRequiredString(JsonLDKeywords.ID).getOrElse(throw AssertionError("No IRI returned"))), + ), + ) val sourceResource1: String = s"""{ @@ -7451,20 +7449,16 @@ class SearchRouteV2R2RSpec extends R2RSpec { | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" | } |}""".stripMargin - - val sourceResource1Iri: IRI = - Post(s"/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, sourceResource1)) ~> addCredentials( - BasicHttpCredentials(anythingUserEmail, password), - ) ~> resourcePath ~> check { - val createSourceResource1ResponseStr = responseAs[String] - assert(response.status == StatusCodes.OK, createSourceResource1ResponseStr) - val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) - val validationFun: (String, => Nothing) => String = - (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) - responseJsonDoc.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) - } - - assert(sourceResource1Iri.toSmartIri.isKnoraDataIri) + val _ = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[TestClientService]( + _.checkResponseOK( + Post( + appConfig.knoraApi.internalKnoraApiBaseUrl + "/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, sourceResource1), + ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)), + ), + ), + ) val sourceResource2: String = s"""{ @@ -7488,19 +7482,16 @@ class SearchRouteV2R2RSpec extends R2RSpec { | } |}""".stripMargin - val sourceResource2Iri: IRI = - Post(s"/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, sourceResource2)) ~> addCredentials( - BasicHttpCredentials(anythingUserEmail, password), - ) ~> resourcePath ~> check { - val createSourceResource2ResponseStr = responseAs[String] - assert(response.status == StatusCodes.OK, createSourceResource2ResponseStr) - val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) - val validationFun: (String, => Nothing) => String = - (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) - responseJsonDoc.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) - } - - assert(sourceResource2Iri.toSmartIri.isKnoraDataIri) + val _ = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[TestClientService]( + _.checkResponseOK( + Post( + appConfig.knoraApi.internalKnoraApiBaseUrl + "/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, sourceResource2), + ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)), + ), + ), + ) val gravsearchQuery = s""" @@ -7762,23 +7753,19 @@ class SearchRouteV2R2RSpec extends R2RSpec { | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" | } |}""".stripMargin - - Post(s"/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials( - BasicHttpCredentials(anythingUserEmail, password), - ) ~> resourcePath ~> check { - val responseStr = responseAs[String] - assert(status == StatusCodes.OK, responseStr) - val resourceCreateResponseAsJsonLD: JsonLDDocument = JsonLDUtil.parseJsonLD(responseStr) - val validationFun: (String, => Nothing) => String = - (s, e) => Iri.validateAndEscapeIri(s).getOrElse(e) - val resourceIri: IRI = - resourceCreateResponseAsJsonLD.body.requireStringWithValidation(JsonLDKeywords.ID, validationFun) - assert(resourceIri.toSmartIri.isKnoraDataIri) - timeTagResourceIri.set(resourceIri) - } + val resourceIri = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[TestClientService]( + _.getResponseJsonLD( + Post( + appConfig.knoraApi.internalKnoraApiBaseUrl + s"/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity), + ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)), + ).map(_.body.getRequiredString(JsonLDKeywords.ID).getOrElse(throw AssertionError("No IRI returned"))), + ), + ) + timeTagResourceIri.set(resourceIri) // Search for the resource. - val gravsearchQuery = """ |PREFIX knora-api: diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala index 0eb14b21d3..d40c08f9df 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala @@ -456,7 +456,7 @@ class CardinalitiesV2E2ESpec extends E2ESpec { ) } - // first adding the the cardinalities to the *super*, then to the *sub* class + // first adding the cardinalities to the *super*, then to the *sub* class val clsAndProps = List( (superClassName, superClassProperty1), (superClassName, superClassProperty2), diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index f59fa13346..ede0d262e4 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -4612,14 +4612,18 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender { permissions = Some("M knora-admin:ProjectMember"), ) - appActor ! CreateResourceRequestV2( - createResource = inputResource, - requestingUser = SharedTestDataADM.imagesUser01, - apiRequestID = randomUUID, + val _ = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.createResource( + CreateResourceRequestV2( + createResource = inputResource, + requestingUser = SharedTestDataADM.imagesUser01, + apiRequestID = randomUUID, + ), + ), + ), ) - expectMsgClass(timeout, classOf[ReadResourcesSequenceV2]) - val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/00FF/images/v2#stueckzahl".toSmartIri val actual = UnsafeZioRun.run( @@ -4655,17 +4659,19 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender { projectADM = SharedTestDataADM.imagesProject, permissions = Some("M knora-admin:ProjectMember"), ) - - appActor ! CreateResourceRequestV2( - createResource = inputResource, - requestingUser = SharedTestDataADM.imagesUser01, - apiRequestID = randomUUID, + val _ = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.createResource( + CreateResourceRequestV2( + createResource = inputResource, + requestingUser = SharedTestDataADM.imagesUser01, + apiRequestID = randomUUID, + ), + ), + ), ) - expectMsgClass(timeout, classOf[ReadResourcesSequenceV2]) - val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/00FF/images/v2#stueckzahl".toSmartIri - UnsafeZioRun.runOrThrow( ZIO.serviceWithZIO[ValuesResponderV2]( _.createValueV2( @@ -4698,17 +4704,19 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender { projectADM = SharedTestDataADM.imagesProject, permissions = Some("M knora-admin:ProjectMember"), ) - - appActor ! CreateResourceRequestV2( - createResource = inputResource, - requestingUser = SharedTestDataADM.imagesUser01, - apiRequestID = randomUUID, + val _ = UnsafeZioRun.runOrThrow( + ZIO.serviceWithZIO[ResourcesResponderV2]( + _.createResource( + CreateResourceRequestV2( + createResource = inputResource, + requestingUser = SharedTestDataADM.imagesUser01, + apiRequestID = randomUUID, + ), + ), + ), ) - expectMsgClass(timeout, classOf[ReadResourcesSequenceV2]) - val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/00FF/images/v2#stueckzahl".toSmartIri - UnsafeZioRun.runOrThrow( ZIO.serviceWithZIO[ValuesResponderV2]( _.createValueV2( diff --git a/webapi/src/main/scala/dsp/errors/Errors.scala b/webapi/src/main/scala/dsp/errors/Errors.scala index 2d29920526..45866b35c4 100644 --- a/webapi/src/main/scala/dsp/errors/Errors.scala +++ b/webapi/src/main/scala/dsp/errors/Errors.scala @@ -150,6 +150,9 @@ object DuplicateValueException { * @param message a description of the error. */ case class OntologyConstraintException(message: String) extends RequestRejectedException(message) +object OntologyConstraintException { + implicit val codec: JsonCodec[OntologyConstraintException] = DeriveJsonCodec.gen[OntologyConstraintException] +} /** * An exception indicating that a requested update is not allowed because another user has edited the @@ -205,7 +208,6 @@ case class InvalidRdfException(msg: String, cause: Throwable = null) extends Req * @param msg a description of the error. */ case class ValidationException(msg: String) extends RequestRejectedException(msg) - object ValidationException { implicit val codec: JsonCodec[ValidationException] = DeriveJsonCodec.gen[ValidationException] } diff --git a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesADM.scala b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesADM.scala deleted file mode 100644 index 85335ebed5..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesADM.scala +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.http.status - -import org.apache.pekko - -import dsp.errors.* - -import pekko.http.scaladsl.model.StatusCode -import pekko.http.scaladsl.model.StatusCodes - -/** - * The possible values for the HTTP status code that is returned as part of each Knora ADM response. - */ -object ApiStatusCodesADM { - - /** - * Converts an exception to a similar HTTP status code. - * - * @param ex an exception. - * @return an HTTP status code. - */ - def fromException(ex: Throwable): StatusCode = - ex match { - // Subclasses of RequestRejectedException (which must be last in this group) - case NotFoundException(_) => StatusCodes.NotFound - case ForbiddenException(_) => StatusCodes.Forbidden - case BadCredentialsException(_) => StatusCodes.Unauthorized - case DuplicateValueException(_) => StatusCodes.BadRequest - case OntologyConstraintException(_) => StatusCodes.BadRequest - case EditConflictException(_) => StatusCodes.Conflict - case RequestRejectedException(_) => StatusCodes.BadRequest - - // Subclasses of InternalServerException (which must be last in this group) - case UpdateNotPerformedException(_) => StatusCodes.Conflict - case InternalServerException(_) => StatusCodes.InternalServerError - case _ => StatusCodes.InternalServerError - } - -} diff --git a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala deleted file mode 100644 index f051b55136..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.http.status - -import zio.http.Status - -import dsp.errors.* -import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException - -/** - * The possible values for the HTTP status code that is returned as part of each Knora API v2 response. - * Migrated from [[org.knora.webapi.http.status.ApiStatusCodes]] - */ -object ApiStatusCodesZ { - - /** - * Converts an exception to a suitable HTTP status code. - * - * @param ex an exception. - * @return an HTTP status code. - */ - - def fromExceptionZ(ex: Throwable): Status = - ex match { - // Subclasses of RequestRejectedException - case NotFoundException(_) => Status.NotFound - case ForbiddenException(_) => Status.Forbidden - case BadCredentialsException(_) => Status.Unauthorized - case DuplicateValueException(_) => Status.BadRequest - case OntologyConstraintException(_) => Status.BadRequest - case EditConflictException(_) => Status.Conflict - case BadRequestException(_) => Status.BadRequest - case ValidationException(_) => Status.BadRequest - case RequestRejectedException(_) => Status.BadRequest - // RequestRejectedException must be the last one in this group - - // Subclasses of InternalServerException - case UpdateNotPerformedException(_) => Status.Conflict - case TriplestoreTimeoutException(_, _) => Status.GatewayTimeout - case InternalServerException(_) => Status.InternalServerError - // InternalServerException must be the last one in this group - - case _ => Status.InternalServerError - } - -} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index c6c5df6455..822863a4e5 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -153,9 +153,6 @@ final case class ResourcesResponderV2( ) => getResourcePreviewV2(resIris, withDeletedResource, targetSchema, requestingUser) - case createResourceRequestV2: CreateResourceRequestV2 => - createHandler(createResourceRequestV2) - case resourceHistoryRequest: ResourceVersionHistoryGetRequestV2 => getResourceHistoryV2(resourceHistoryRequest) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala index 8885fbd2a5..edad53dc37 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -82,9 +82,7 @@ final case class ApiRoutes( shaclApiRoutes.routes ).reduce(_ ~ _) val pekkoRoutes = - OntologiesRouteV2().makeRoute ~ - ResourcesRouteV2(appConfig).makeRoute ~ - StandoffRouteV2().makeRoute + OntologiesRouteV2().makeRoute ~ StandoffRouteV2().makeRoute tapirRoutes ~ pekkoRoutes } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala deleted file mode 100644 index 97965131c4..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.routing.v2 - -import com.typesafe.scalalogging.LazyLogging -import org.apache.pekko.http.scaladsl.server.Directives.* -import org.apache.pekko.http.scaladsl.server.PathMatcher -import org.apache.pekko.http.scaladsl.server.Route -import zio.* -import zio.ZIO - -import dsp.errors.BadRequestException -import org.knora.webapi.* -import org.knora.webapi.config.AppConfig -import org.knora.webapi.config.Sipi -import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.v2.responder.resourcemessages.* -import org.knora.webapi.messages.v2.responder.valuemessages.* -import org.knora.webapi.responders.v2.SearchResponderV2 -import org.knora.webapi.routing.RouteUtilV2 -import org.knora.webapi.routing.RouteUtilZ -import org.knora.webapi.slice.admin.domain.service.ProjectService -import org.knora.webapi.slice.admin.domain.service.UserService -import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser -import org.knora.webapi.slice.resourceinfo.domain.IriConverter -import org.knora.webapi.slice.security.Authenticator -import org.knora.webapi.store.iiif.api.SipiService - -/** - * Provides a routing function for API v2 routes that deal with resources. - */ -final case class ResourcesRouteV2(appConfig: AppConfig)( - private implicit val runtime: Runtime[ - ApiComplexV2JsonLdRequestParser & AppConfig & Authenticator & IriConverter & ProjectService & MessageRelay & - SearchResponderV2 & SipiService & StringFormatter & UserService, - ], -) extends LazyLogging { - - private val jsonLdRequestParser = ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser] - private val sipiConfig: Sipi = appConfig.sipi - private val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") - - def makeRoute: Route = createResource() - - private def createResource(): Route = path(resourcesBasePath) { - post { - entity(as[String]) { jsonRequest => requestContext => - val requestTask = for { - requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - apiRequestId <- RouteUtilZ.randomUuid() - requestMessage <- jsonLdRequestParser( - _.createResourceRequestV2(jsonRequest, requestingUser, apiRequestId), - ).mapError(BadRequestException.apply) - // check for each value which represents a file value if the file's MIME type is allowed - _ <- checkMimeTypesForFileValueContents(requestMessage.createResource.flatValues) - } yield requestMessage - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - } - - /** - * Checks if the MIME types of the given values are allowed by the configuration - * - * @param values the values to be checked. - */ - private def checkMimeTypesForFileValueContents( - values: Iterable[CreateValueInNewResourceV2], - ): Task[Unit] = { - def failBadRequest(fileValueContent: FileValueContentV2): IO[BadRequestException, Unit] = { - val msg = - s"File ${fileValueContent.fileValue.internalFilename} has MIME type ${fileValueContent.fileValue.internalMimeType}, which is not supported for still image files" - ZIO.fail(BadRequestException(msg)) - } - ZIO - .foreach(values) { value => - value.valueContent match { - case fileValueContent: StillImageFileValueContentV2 => - failBadRequest(fileValueContent) - .when(!sipiConfig.imageMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) - case fileValueContent: DocumentFileValueContentV2 => - failBadRequest(fileValueContent) - .when(!sipiConfig.documentMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) - case fileValueContent: ArchiveFileValueContentV2 => - failBadRequest(fileValueContent) - .when(!sipiConfig.archiveMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) - case fileValueContent: TextFileValueContentV2 => - failBadRequest(fileValueContent) - .when(!sipiConfig.textMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) - case fileValueContent: AudioFileValueContentV2 => - failBadRequest(fileValueContent) - .when(!sipiConfig.audioMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) - case fileValueContent: MovingImageFileValueContentV2 => - failBadRequest(fileValueContent) - .when(!sipiConfig.videoMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) - case _ => ZIO.unit - } - } - .unit - } -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala index fecd4cc1db..377b68fea5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParser.scala @@ -18,6 +18,7 @@ import scala.jdk.CollectionConverters.* import scala.language.implicitConversions import org.knora.webapi.IRI +import org.knora.webapi.config.Sipi import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex as KA import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.* @@ -54,6 +55,7 @@ final case class ApiComplexV2JsonLdRequestParser( sipiService: SipiService, projectService: ProjectService, userService: UserService, + sipiConfig: Sipi, ) { /** @@ -243,19 +245,52 @@ final case class ApiComplexV2JsonLdRequestParser( .when(r.resourceIri.exists(_.shortcode != project.shortcode)) attachedToUser <- attachedToUser(r.resource, requestingUser, project.id) values <- extractValues(r.resource, project.shortcode) - } yield CreateResourceRequestV2( - CreateResourceV2( - r.resourceIri.map(_.smartIri), - r.resourceClassSmartIri, - label, - values, - project, - permissions, - creationDate, - ), - attachedToUser, - uuid, - ) + createResource = CreateResourceV2( + r.resourceIri.map(_.smartIri), + r.resourceClassSmartIri, + label, + values, + project, + permissions, + creationDate, + ) + _ <- checkMimeTypesForFileValueContents(createResource.flatValues) + } yield CreateResourceRequestV2(createResource, attachedToUser, uuid) + } + + private def checkMimeTypesForFileValueContents( + values: Iterable[CreateValueInNewResourceV2], + ): IO[String, Unit] = { + def failBadRequest(fileValueContent: FileValueContentV2): IO[String, Unit] = { + val msg = + s"File ${fileValueContent.fileValue.internalFilename} has MIME type ${fileValueContent.fileValue.internalMimeType}, which is not supported for still image files" + ZIO.fail(msg) + } + ZIO + .foreach(values) { value => + value.valueContent match { + case fileValueContent: StillImageFileValueContentV2 => + failBadRequest(fileValueContent) + .when(!sipiConfig.imageMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) + case fileValueContent: DocumentFileValueContentV2 => + failBadRequest(fileValueContent) + .when(!sipiConfig.documentMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) + case fileValueContent: ArchiveFileValueContentV2 => + failBadRequest(fileValueContent) + .when(!sipiConfig.archiveMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) + case fileValueContent: TextFileValueContentV2 => + failBadRequest(fileValueContent) + .when(!sipiConfig.textMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) + case fileValueContent: AudioFileValueContentV2 => + failBadRequest(fileValueContent) + .when(!sipiConfig.audioMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) + case fileValueContent: MovingImageFileValueContentV2 => + failBadRequest(fileValueContent) + .when(!sipiConfig.videoMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) + case _ => ZIO.unit + } + } + .unit } private def extractValues( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala index 38c4df3eb0..62f290ca85 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala @@ -37,6 +37,9 @@ final case class BaseEndpoints(authenticator: Authenticator)(implicit val r: zio // default oneOfVariant[NotFoundException](statusCode(StatusCode.NotFound).and(jsonBody[NotFoundException])), oneOfVariant[BadRequestException](statusCode(StatusCode.BadRequest).and(jsonBody[BadRequestException])), + oneOfVariant[OntologyConstraintException]( + statusCode(StatusCode.BadRequest).and(jsonBody[OntologyConstraintException]), + ), oneOfVariant[ValidationException](statusCode(StatusCode.BadRequest).and(jsonBody[ValidationException])), oneOfVariant[DuplicateValueException](statusCode(StatusCode.BadRequest).and(jsonBody[DuplicateValueException])), oneOfVariant[GravsearchException](statusCode(StatusCode.BadRequest).and(jsonBody[GravsearchException])), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index 6db67afc78..359e1f1448 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -118,13 +118,6 @@ final case class ResourcesEndpoints( .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) - val postResourcesErase = baseEndpoints.withUserEndpoint.post - .in(base / "erase") - .in(ApiV2.Inputs.formatOptions) - .in(stringJsonBody) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) - val getResourcesTei = baseEndpoints.withUserEndpoint.get .in("v2" / "tei" / path[IriDto].name("resourceIri")) .in(query[Option[IriDto]]("mappingIri")) @@ -134,6 +127,13 @@ final case class ResourcesEndpoints( .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) + val postResourcesErase = baseEndpoints.withUserEndpoint.post + .in(base / "erase") + .in(ApiV2.Inputs.formatOptions) + .in(stringJsonBody) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val postResourcesDelete = baseEndpoints.withUserEndpoint.post .in(base / "delete") .in(ApiV2.Inputs.formatOptions) @@ -141,6 +141,13 @@ final case class ResourcesEndpoints( .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) + val postResources = baseEndpoints.withUserEndpoint.post + .in(base) + .in(ApiV2.Inputs.formatOptions) + .in(stringJsonBody) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + val putResources = baseEndpoints.withUserEndpoint.put .in(base) .in(ApiV2.Inputs.formatOptions) @@ -160,6 +167,7 @@ final case class ResourcesEndpoints( getResourcesTei, postResourcesErase, postResourcesDelete, + postResources, putResources, ).map(_.endpoint.tag("V2 Resources")) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index 719871a1dc..7dd2301240 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -44,6 +44,7 @@ final class ResourcesEndpointsHandler( SecuredEndpointHandler(resourcesEndpoints.getResourcesTei, resourcesRestService.getResourceAsTeiV2), SecuredEndpointHandler(resourcesEndpoints.postResourcesErase, resourcesRestService.eraseResource), SecuredEndpointHandler(resourcesEndpoints.postResourcesDelete, resourcesRestService.deleteResource), + SecuredEndpointHandler(resourcesEndpoints.postResources, resourcesRestService.createResource), SecuredEndpointHandler(resourcesEndpoints.putResources, resourcesRestService.updateResourceMetadata), ).map(mapper.mapSecuredEndpointHandler(_)) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala index a49a55e513..73917f3b70 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourcesRestService.scala @@ -158,6 +158,14 @@ final case class ResourcesRestService( response <- renderer.render(result, formatOptions) } yield response + def createResource(user: User)(formatOptions: FormatOptions, jsonLd: String): Task[(RenderedResponse, MediaType)] = + for { + uuid <- Random.nextUUID + createRequest <- requestParser.createResourceRequestV2(jsonLd, user, uuid).mapError(BadRequestException.apply) + result <- resourcesService.createResource(createRequest) + response <- renderer.render(result, formatOptions) + } yield response + def updateResourceMetadata( user: User, )(formatOptions: FormatOptions, jsonLd: String): Task[(RenderedResponse, MediaType)] = From a1f5285975424c2758686b22e2760f35744605c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 4 Mar 2025 11:36:36 +0100 Subject: [PATCH 15/15] address Codacy issue "Avoid using return" --- .../resourcemessages/ResourceMessagesV2.scala | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index 3d9b845512..2738490f56 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -839,35 +839,25 @@ case class ReadResourcesSequenceV2( } /** - * Checks that requested resources were found and that the user has permission to see them. If not, throws an exception. + * Checks that requested resources were found and that the user has permission to see them. If not: + * Fails with a [[NotFoundException]] if the requested resources are not found. + * Fails with a [[ForbiddenException]] if the user does not have permission to see the requested resources. * * @param targetResourceIris the IRIs to be checked. * @param resourcesSequence the result of requesting those IRIs. - * @throws NotFoundException if the requested resources are not found. - * @throws ForbiddenException if the user does not have permission to see the requested resources. */ - def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Task[Unit] = { - val hiddenTargetResourceIris: Set[IRI] = targetResourceIris.intersect(resourcesSequence.hiddenResourceIris) - - if (hiddenTargetResourceIris.nonEmpty) { - return ZIO.fail( - ForbiddenException( - s"You do not have permission to see one or more resources: ${hiddenTargetResourceIris.map(iri => s"<$iri>").mkString(", ")}", - ), - ) - } - - val missingResourceIris: Set[IRI] = targetResourceIris -- resourcesSequence.resources.map(_.resourceIri).toSet - - if (missingResourceIris.nonEmpty) { - return ZIO.fail( - NotFoundException( - s"One or more resources were not found: ${missingResourceIris.map(iri => s"<$iri>").mkString(", ")}", - ), - ) - } - ZIO.unit - } + def checkResourceIris(targetResourceIris: Set[IRI], resourcesSequence: ReadResourcesSequenceV2): Task[Unit] = + targetResourceIris.intersect(resourcesSequence.hiddenResourceIris) match + case hiddenTargetResourceIris if hiddenTargetResourceIris.nonEmpty => + lazy val msg = + s"You do not have permission to see one or more resources: ${hiddenTargetResourceIris.map(iri => s"<$iri>").mkString(", ")}" + ZIO.fail(ForbiddenException(msg)) + case _ => { + val missingResourceIris = targetResourceIris -- resourcesSequence.resources.map(_.resourceIri).toSet + lazy val msg = + s"One or more resources were not found: ${missingResourceIris.map(iri => s"<$iri>").mkString(", ")}" + ZIO.when(missingResourceIris.nonEmpty)(ZIO.fail(NotFoundException(msg))).unit + } /** * Considers this [[ReadResourcesSequenceV2]] to be the result of an update operation in a single project