From e79a2f9e97c194fb9a1cc0d277284e527b22679f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 13 Nov 2024 10:09:22 +0100 Subject: [PATCH] feat: Fully support json-ld for delete value and update, delete or erase resource (#3422) --- .../resourcemessages/ResourceMessagesV2.scala | 152 ---------- .../valuemessages/ValueMessagesV2.scala | 64 ---- .../webapi/routing/v2/ResourcesRouteV2.scala | 27 +- .../webapi/routing/v2/ValuesRouteV2.scala | 19 +- .../ApiComplexV2JsonLdRequestParser.scala | 278 ++++++++++++------ 5 files changed, 208 insertions(+), 332 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 44bc97920f..43a2623aed 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,6 @@ package org.knora.webapi.messages.v2.responder.resourcemessages -import zio.* - import java.time.Instant import java.util.UUID @@ -21,7 +19,6 @@ import org.knora.webapi.messages.OntologyConstants.* import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.ValuesValidator.xsdDateTimeStampToInstant import org.knora.webapi.messages.util.* import org.knora.webapi.messages.util.rdf.* import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 @@ -35,7 +32,6 @@ 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 -import org.knora.webapi.slice.resourceinfo.domain.IriConverter /** * An abstract trait for messages that can be sent to `ResourcesResponderV2`. @@ -667,97 +663,6 @@ case class UpdateResourceMetadataRequestV2( apiRequestID: UUID, ) extends ResourcesResponderRequestV2 -object UpdateResourceMetadataRequestV2 { - - /** - * Converts JSON-LD input into an instance of [[UpdateResourceMetadataRequestV2]]. - * - * @param jsonLDDocument the JSON-LD input. - * @param apiRequestID the UUID of the API request. - * @param requestingUser the user making the request. - * @return a case class instance representing the input. - */ - def fromJsonLD( - jsonLDDocument: JsonLDDocument, - requestingUser: User, - apiRequestID: UUID, - ): ZIO[StringFormatter & IriConverter, Throwable, UpdateResourceMetadataRequestV2] = { - val body = jsonLDDocument.body - for { - resourceIri <- getResourceIri(body) - resourceClassIri <- getResourceClassIri(body) - maybeLastModificationDate <- getModificationTimestamp(KnoraApiV2Complex.LastModificationDate, body) - maybeLabel <- getLabel(body) - maybePermissions <- getPermissions(body) - maybeNewModificationDate <- getModificationTimestamp(KnoraApiV2Complex.NewModificationDate, body) - _ <- ZIO - .fail(BadRequestException(s"No updated resource metadata provided")) - .when(areAllOptionsEmpty(maybeLabel, maybePermissions, maybeNewModificationDate)) - } yield UpdateResourceMetadataRequestV2( - resourceIri.toString, - resourceClassIri, - maybeLastModificationDate, - maybeLabel, - maybePermissions, - maybeNewModificationDate, - requestingUser, - apiRequestID, - ) - } - - private def getResourceIri(obj: JsonLDObject): ZIO[StringFormatter & IriConverter, Throwable, SmartIri] = - for { - resourceIri <- obj.getRequiredIdValueAsKnoraDataIri - .filterOrElseWith(_.isKnoraResourceIri)(it => ZIO.fail(s"Invalid resource IRI: <$it>")) - .mapError(BadRequestException(_)) - } yield resourceIri - - private def getResourceClassIri(body: JsonLDObject) = - body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_)) - - private def getLabel(obj: JsonLDObject): IO[BadRequestException, Option[IRI]] = { - val getLabel = for { - labelStr <- ZIO.fromEither(obj.getString(Rdfs.Label)) - label <- ZIO.foreach(labelStr)(it => - ZIO - .fromOption(Iri.toSparqlEncodedString(it)) - .orElseFail(s"Invalid label: $it"), - ) - } yield label - getLabel.mapError(BadRequestException(_)) - } - - private def getPermissions(obj: JsonLDObject): IO[BadRequestException, Option[String]] = { - val key = KnoraApiV2Complex.HasPermissions - val getPerms = for { - permsMaybe <- ZIO.fromEither(obj.getString(KnoraApiV2Complex.HasPermissions)) - perms <- ZIO.foreach(permsMaybe)(it => - ZIO - .fromOption(Iri.toSparqlEncodedString(it)) - .orElseFail(s"Invalid $key: $it"), - ) - } yield perms - getPerms.mapError(BadRequestException(_)) - } - private def getModificationTimestamp( - key: IRI, - obj: JsonLDObject, - ): ZIO[IriConverter, BadRequestException, Option[Instant]] = { - val getTimeStamp = for { - tsDataType <- ZIO.serviceWithZIO[IriConverter](_.asSmartIri(Xsd.DateTimeStamp)).orDie - tsString <- obj.getDataTypeValueInObject(key, tsDataType) - tsDate <- ZIO.foreach(tsString)(tsStr => - ZIO - .fromOption(xsdDateTimeStampToInstant(tsStr)) - .orElseFail(s"Invalid datatype value literal: $tsStr"), - ) - } yield tsDate - getTimeStamp.mapError(BadRequestException(_)) - } - - private def areAllOptionsEmpty(options: Option[?]*): Boolean = options.forall(_.isEmpty) -} - /** * Represents a response after updating a resource's metadata. * @@ -863,63 +768,6 @@ case class DeleteOrEraseResourceRequestV2( apiRequestID: UUID, ) extends ResourcesResponderRequestV2 -object DeleteOrEraseResourceRequestV2 { - - /** - * Converts JSON-LD input into an instance of [[DeleteOrEraseResourceRequestV2]]. - * - * @param jsonLDDocument the JSON-LD input. - * @param apiRequestID the UUID of the API request. - * @param requestingUser the user making the request. - * @return a case class instance representing the input. - */ - def fromJsonLD( - jsonLDDocument: JsonLDDocument, - requestingUser: User, - apiRequestID: UUID, - ): ZIO[StringFormatter & IriConverter, Throwable, DeleteOrEraseResourceRequestV2] = - ZIO.serviceWithZIO[StringFormatter] { implicit sf => - for { - resourceIri <- - jsonLDDocument.body.getRequiredIdValueAsKnoraDataIri - .mapError(BadRequestException(_)) - .tap(iri => - ZIO.when(!iri.isKnoraResourceIri)(ZIO.fail(BadRequestException(s"Invalid resource IRI: <$iri>"))), - ) - - resourceClassIri <- - jsonLDDocument.body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_)) - - maybeLastModificationDate: Option[Instant] = jsonLDDocument.body.maybeDatatypeValueInObject( - key = KnoraApiV2Complex.LastModificationDate, - expectedDatatype = Xsd.DateTimeStamp.toSmartIri, - validationFun = (s, errorFun) => - xsdDateTimeStampToInstant(s).getOrElse(errorFun), - ) - - maybeDeleteComment: Option[String] = jsonLDDocument.body.maybeStringWithValidation( - KnoraApiV2Complex.DeleteComment, - (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun), - ) - - maybeDeleteDate: Option[Instant] = jsonLDDocument.body.maybeDatatypeValueInObject( - KnoraApiV2Complex.DeleteDate, - Xsd.DateTimeStamp.toSmartIri, - (s, errorFun) => xsdDateTimeStampToInstant(s).getOrElse(errorFun), - ) - - } yield DeleteOrEraseResourceRequestV2( - resourceIri = resourceIri.toString, - resourceClassIri = resourceClassIri, - maybeDeleteComment = maybeDeleteComment, - maybeDeleteDate = maybeDeleteDate, - maybeLastModificationDate = maybeLastModificationDate, - requestingUser = requestingUser, - apiRequestID = apiRequestID, - ) - } -} - /** * Represents a sequence of resources read back from Knora. * diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 8f33357ead..96a489e46a 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -19,7 +19,6 @@ import dsp.errors.AssertionException import dsp.errors.BadRequestException import dsp.errors.NotFoundException import dsp.valueobjects.Iri -import dsp.valueobjects.IriErrorMessages import dsp.valueobjects.UuidUtil import org.knora.webapi.* import org.knora.webapi.config.AppConfig @@ -40,7 +39,6 @@ import org.knora.webapi.messages.v2.responder.* import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages.* import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo -import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.routing.RouteUtilZ import org.knora.webapi.routing.v2.AssetIngestState import org.knora.webapi.routing.v2.AssetIngestState.* @@ -171,68 +169,6 @@ case class DeleteValueV2( deleteDate: Option[Instant] = None, ) -object DeleteValueV2 { - - /** - * Converts JSON-LD input into a case class instance. - * - * @param jsonLdString the JSON-LD input as String. - * @return a case class instance representing the input. - */ - def fromJsonLd(jsonLdString: String): ZIO[StringFormatter & IriConverter, Throwable, DeleteValueV2] = - ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => - RouteUtilV2.parseJsonLd(jsonLdString).flatMap { jsonLDDocument => - jsonLDDocument.body.getRequiredResourcePropertyApiV2ComplexValue.mapError(BadRequestException(_)).flatMap { - case (propertyIri: SmartIri, jsonLDObject: JsonLDObject) => - for { - resourceIri <- - jsonLDDocument.body.getRequiredIdValueAsKnoraDataIri - .mapError(BadRequestException(_)) - .tap(iri => - ZIO.fail(BadRequestException(s"Invalid resource IRI: <$iri>")).when(!iri.isKnoraResourceIri), - ) - - resourceClassIri <- - jsonLDDocument.body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_)) - valueIri <- jsonLDObject.getRequiredIdValueAsKnoraDataIri.mapError(BadRequestException(_)) - _ <- ZIO.fail(BadRequestException(s"Invalid value IRI: <$valueIri>")).when(!valueIri.isKnoraValueIri) - _ <- ZIO - .fail(BadRequestException(IriErrorMessages.UuidVersionInvalid)) - .when( - UuidUtil.hasValidLength(UuidUtil.fromIri(valueIri.toString)) && - !UuidUtil.hasSupportedVersion(valueIri.toString), - ) - valueTypeIri <- jsonLDObject.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_)) - deleteComment <- ZIO.attempt { - val validationFun: (String, => Nothing) => String = - (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) - jsonLDObject.maybeStringWithValidation( - OntologyConstants.KnoraApiV2Complex.DeleteComment, - validationFun, - ) - } - deleteDate <- ZIO.attempt( - jsonLDObject.maybeDatatypeValueInObject( - key = OntologyConstants.KnoraApiV2Complex.DeleteDate, - expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, - validationFun = - (s, errorFun) => ValuesValidator.xsdDateTimeStampToInstant(s).getOrElse(errorFun), - ), - ) - } yield DeleteValueV2( - resourceIri = resourceIri.toString, - resourceClassIri = resourceClassIri, - propertyIri = propertyIri, - valueIri = valueIri.toString, - valueTypeIri = valueTypeIri, - deleteComment = deleteComment, - deleteDate = deleteDate, - ) - } - } - } -} - case class GenerateSparqlForValueInNewResourceV2( valueContent: ValueContentV2, customValueIri: Option[SmartIri], 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 6f4e12419a..3ea0850490 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 @@ -26,7 +26,6 @@ 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.util.rdf.JsonLDUtil import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.v2.SearchResponderV2 @@ -49,6 +48,9 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( SearchResponderV2 & SipiService & StringFormatter & UserService, ], ) extends LazyLogging { + + 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 @@ -100,15 +102,12 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( entity(as[String]) { jsonRequest => requestContext => { val requestTask = for { - requestDoc <- RouteUtilV2.parseJsonLd(jsonRequest) requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) apiRequestId <- RouteUtilZ.randomUuid() ingestState = AssetIngestState.headerAssetIngestState(requestContext.request.headers) - requestMessage <- ZIO - .serviceWithZIO[ApiComplexV2JsonLdRequestParser]( - _.createResourceRequestV2(jsonRequest, ingestState, requestingUser, apiRequestId), - ) - .mapError(BadRequestException(_)) + requestMessage <- jsonLdRequestParser( + _.createResourceRequestV2(jsonRequest, ingestState, 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 @@ -123,10 +122,11 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( entity(as[String]) { jsonRequest => requestContext => { val requestMessageFuture = for { - requestDoc <- ZIO.attempt(JsonLDUtil.parseJsonLD(jsonRequest)) requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) apiRequestId <- RouteUtilZ.randomUuid() - requestMessage <- UpdateResourceMetadataRequestV2.fromJsonLD(requestDoc, requestingUser, apiRequestId) + requestMessage <- + jsonLdRequestParser(_.updateResourceMetadataRequestV2(jsonRequest, requestingUser, apiRequestId)) + .mapError(BadRequestException.apply) } yield requestMessage RouteUtilV2.runRdfRouteZ(requestMessageFuture, requestContext) } @@ -383,10 +383,10 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( entity(as[String]) { jsonRequest => requestContext => { val requestTask = for { - requestDoc <- ZIO.attempt(JsonLDUtil.parseJsonLD(jsonRequest)) apiRequestId <- RouteUtilZ.randomUuid() requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - msg <- DeleteOrEraseResourceRequestV2.fromJsonLD(requestDoc, requestingUser, apiRequestId) + msg <- jsonLdRequestParser(_.deleteOrEraseResourceRequestV2(jsonRequest, requestingUser, apiRequestId)) + .mapError(BadRequestException.apply) } yield msg RouteUtilV2.runRdfRouteZ(requestTask, requestContext) } @@ -399,10 +399,11 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( entity(as[String]) { jsonRequest => requestContext => { val requestTask = for { - requestDoc <- ZIO.attempt(JsonLDUtil.parseJsonLD(jsonRequest)) apiRequestId <- RouteUtilZ.randomUuid() requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) - requestMessage <- DeleteOrEraseResourceRequestV2.fromJsonLD(requestDoc, requestingUser, apiRequestId) + requestMessage <- + jsonLdRequestParser(_.deleteOrEraseResourceRequestV2(jsonRequest, requestingUser, apiRequestId)) + .mapError(BadRequestException.apply) } yield requestMessage.copy(erase = true) RouteUtilV2.runRdfRouteZ(requestTask, requestContext) } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala index abee80b6d2..5944972226 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala @@ -17,7 +17,6 @@ import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.v2.responder.resourcemessages.ResourcesGetRequestV2 -import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.v2.ValuesResponderV2 import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.routing.RouteUtilZ @@ -36,6 +35,8 @@ final case class ValuesRouteV2()( ], ) { + private val jsonLdRequestParser = ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser] + private val responder = ZIO.serviceWithZIO[ValuesResponderV2] private val valuesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "values") def makeRoute: Route = getValue() ~ createValue() ~ updateValue() ~ deleteValue() @@ -86,11 +87,10 @@ final case class ValuesRouteV2()( requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(ctx)) apiRequestId <- Random.nextUUID ingestState = AssetIngestState.headerAssetIngestState(ctx.request.headers) - valueToCreate <- ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser]( + valueToCreate <- jsonLdRequestParser( _.createValueV2FromJsonLd(jsonLdString, ingestState).mapError(BadRequestException(_)), ) - response <- - ZIO.serviceWithZIO[ValuesResponderV2](_.createValueV2(valueToCreate, requestingUser, apiRequestId)) + response <- responder(_.createValueV2(valueToCreate, requestingUser, apiRequestId)) } yield response, ctx, ) @@ -108,11 +108,10 @@ final case class ValuesRouteV2()( requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(ctx)) apiRequestId <- Random.nextUUID ingestState = AssetIngestState.headerAssetIngestState(ctx.request.headers) - updateValue <- ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser]( + updateValue <- jsonLdRequestParser( _.updateValueV2fromJsonLd(jsonLdString, ingestState).mapError(BadRequestException(_)), ) - response <- - ZIO.serviceWithZIO[ValuesResponderV2](_.updateValueV2(updateValue, requestingUser, apiRequestId)) + response <- responder(_.updateValueV2(updateValue, requestingUser, apiRequestId)) } yield response, ctx, ) @@ -129,9 +128,9 @@ final case class ValuesRouteV2()( for { requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext)) apiRequestId <- RouteUtilZ.randomUuid() - deleteValue <- DeleteValueV2.fromJsonLd(jsonLdString) - response <- - ZIO.serviceWithZIO[ValuesResponderV2](_.deleteValueV2(deleteValue, requestingUser, apiRequestId)) + deleteValue <- + jsonLdRequestParser(_.deleteValueV2FromJsonLd(jsonLdString).mapError(BadRequestException(_))) + response <- responder(_.deleteValueV2(deleteValue, requestingUser, apiRequestId)) } yield response, requestContext, ) 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 a9b08dcc7e..a8a941e433 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 @@ -19,16 +19,17 @@ import scala.language.implicitConversions import dsp.valueobjects.UuidUtil import org.knora.webapi.IRI import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.OntologyConstants -import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.* import org.knora.webapi.messages.OntologyConstants.Rdfs import org.knora.webapi.messages.OntologyConstants.Xsd import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.ValuesValidator +import org.knora.webapi.messages.ValuesValidator.parseXsdDateTimeStamp import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceRequestV2 import org.knora.webapi.messages.v2.responder.resourcemessages.CreateResourceV2 import org.knora.webapi.messages.v2.responder.resourcemessages.CreateValueInNewResourceV2 +import org.knora.webapi.messages.v2.responder.resourcemessages.DeleteOrEraseResourceRequestV2 +import org.knora.webapi.messages.v2.responder.resourcemessages.UpdateResourceMetadataRequestV2 import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo import org.knora.webapi.routing.v2.AssetIngestState @@ -58,42 +59,172 @@ final case class ApiComplexV2JsonLdRequestParser( userService: UserService, ) { + /** + * Every value or resource request MUST only contain a single root rdf resource. + * The root resource MUST have a rdf:type property that specifies the Knora resource class. + * The root resource MAY be an uri resource or a blank node resource. + * The ResourceAccessors trait provides some common methods to access the properties of the root resource. + */ + private trait ResourceAccessors { + def resource: Resource + + def resourceClassIri: ResourceClassIri + def resourceClassSmartIri: SmartIri = resourceClassIri.smartIri + + // accessor methods for various properties of the root resource + def deleteComment: IO[String, Option[String]] = ZIO.fromEither(resource.objectStringOption(DeleteComment)) + def deleteDate: IO[String, Option[Instant]] = instantOption(resource, DeleteDate) + def hasPermissionsOption: IO[String, Option[String]] = ZIO.fromEither(resource.objectStringOption(HasPermissions)) + def creationDate: IO[String, Option[Instant]] = instantOption(resource, CreationDate) + def lastModificationDateOption: IO[String, Option[Instant]] = instantOption(resource, LastModificationDate) + def newModificationDateOption: IO[String, Option[Instant]] = instantOption(resource, NewModificationDate) + def rdfsLabelOption: IO[String, Option[String]] = ZIO.fromEither(resource.objectStringOption(Rdfs.Label)) + } + + private case class RootUriResource(resource: Resource, resourceIri: ResourceIri, resourceClassIri: ResourceClassIri) + extends ResourceAccessors { + def resourceIriStr: String = resourceIri.smartIri.toIri + def shortcode: Shortcode = resourceIri.shortcode + } + private object RootUriResource { + def fromJsonLd(str: String): ZIO[Scope, String, RootUriResource] = + for { + r <- RootResource.fromJsonLd(str) + iri <- ZIO.fromOption(r.resourceIri).orElseFail("No resource IRI found") + } yield RootUriResource(r.resource, iri, r.resourceClassIri) + } + + private case class RootResource( + resource: Resource, + resourceIri: Option[ResourceIri], + resourceClassIri: ResourceClassIri, + ) extends ResourceAccessors + private object RootResource { + def fromJsonLd(str: String): ZIO[Scope, String, RootResource] = + for { + model <- ModelOps.fromJsonLd(str) + resource <- ZIO.fromEither(model.singleRootResource) + resourceIriOption <- + ZIO + .foreach(resource.uri)( + converter.asSmartIri(_).mapError(_.getMessage).flatMap(iri => ZIO.fromEither(KResourceIri.from(iri))), + ) + resourceClassIri <- resourceClassIri(resource) + } yield RootResource(resource, resourceIriOption, resourceClassIri) + + private def resourceClassIri(r: Resource): IO[String, ResourceClassIri] = ZIO + .fromOption(r.rdfsType) + .orElseFail("No root resource class IRI found") + .flatMap(converter.asSmartIri(_).mapError(_.getMessage)) + .flatMap(iri => ZIO.fromEither(KResourceClassIri.fromApiV2Complex(iri))) + } + + def updateResourceMetadataRequestV2( + str: String, + requestingUser: User, + uuid: UUID, + ): IO[String, UpdateResourceMetadataRequestV2] = ZIO.scoped { + for { + r <- RootUriResource.fromJsonLd(str) + label <- r.rdfsLabelOption + permissions <- r.hasPermissionsOption + lastModificationDate <- r.lastModificationDateOption + newModificationDate <- r.newModificationDateOption + _ <- ZIO + .fail("No updated resource metadata provided") + .when(label.isEmpty && permissions.isEmpty && newModificationDate.isEmpty) + } yield UpdateResourceMetadataRequestV2( + r.resourceIriStr, + r.resourceClassSmartIri, + lastModificationDate, + label, + permissions, + newModificationDate, + requestingUser, + uuid, + ) + } + + def deleteOrEraseResourceRequestV2( + str: String, + requestingUser: User, + uuid: UUID, + ): IO[String, DeleteOrEraseResourceRequestV2] = ZIO.scoped { + for { + r <- RootUriResource.fromJsonLd(str) + deleteComment <- r.deleteComment + deleteDate <- r.deleteDate + lastModificationDate <- r.lastModificationDateOption + } yield DeleteOrEraseResourceRequestV2( + r.resourceIriStr, + r.resourceClassSmartIri, + deleteComment, + deleteDate, + lastModificationDate, + false, + requestingUser, + uuid, + ) + } + + private def instantOption(r: Resource, p: Property) = + ZIO.fromEither(r.objectDataTypeOption(p, Xsd.DateTimeStamp)).flatMap { option => + ZIO.foreach(option)(dateStr => ZIO.fromEither(parseXsdDateTimeStamp(dateStr))) + } + + def deleteValueV2FromJsonLd(str: String): IO[String, DeleteValueV2] = ZIO.scoped { + for { + r <- RootUriResource.fromJsonLd(str) + valueStatement <- valueStatement(r.resource) + valueResource = valueStatement.getObject.asResource() + valueIri <- valueIri(valueResource).someOrFail("The value IRI is required") + valueTypeIri <- valueType(valueResource) + propertyIri <- valuePropertyIri(valueStatement) + valueDeleteDate <- instantOption(valueResource, DeleteDate) + valueDeleteComment <- ZIO.fromEither(valueResource.objectStringOption(DeleteComment)) + } yield DeleteValueV2( + r.resourceIriStr, + r.resourceClassSmartIri, + propertyIri.smartIri, + valueIri.smartIri.toIri, + valueTypeIri, + valueDeleteComment, + valueDeleteDate, + ) + } + def createResourceRequestV2( str: String, ingestState: AssetIngestState, requestingUser: User, uuid: UUID, - ): IO[String, CreateResourceRequestV2] = - ZIO.scoped { - for { - model <- ModelOps.fromJsonLd(str) - resourceAndIri <- resourceAndIriOption(model) - (resource, resourceIri) = resourceAndIri - resourceClassIri <- resourceClassIri(resource) - label <- ZIO.fromEither(resource.objectString(Rdfs.Label)) - project <- attachedToProject(resource) - _ <- ZIO - .fail("Resource IRI and project IRI must reference the same project.") - .when(resourceIri.exists(_.shortcode != project.getShortcode)) - permissions <- ZIO.fromEither(resource.objectStringOption(HasPermissions)) - attachedToUser <- attachedToUser(resource, requestingUser, project.projectIri) - creationDate <- creationDate(resource) - values <- extractValues(resource, project.getShortcode, ingestState) - } yield CreateResourceRequestV2( - CreateResourceV2( - resourceIri.map(_.smartIri), - resourceClassIri.smartIri, - label, - values, - project, - permissions, - creationDate, - ), - attachedToUser, - uuid, - ingestState, - ) - } + ): IO[String, CreateResourceRequestV2] = ZIO.scoped { + for { + r <- RootResource.fromJsonLd(str) + permissions <- r.hasPermissionsOption + creationDate <- r.creationDate + label <- r.rdfsLabelOption.someOrFail("A Resource must have an rdfs:label") + project <- attachedToProject(r.resource) + _ <- ZIO + .fail("Resource IRI and project IRI must reference the same project") + .when(r.resourceIri.exists(_.shortcode != project.getShortcode)) + attachedToUser <- attachedToUser(r.resource, requestingUser, project.projectIri) + values <- extractValues(r.resource, project.getShortcode, ingestState) + } yield CreateResourceRequestV2( + CreateResourceV2( + r.resourceIri.map(_.smartIri), + r.resourceClassSmartIri, + label, + values, + project, + permissions, + creationDate, + ), + attachedToUser, + uuid, + ingestState, + ) + } private def extractValues( r: Resource, @@ -103,10 +234,10 @@ final case class ApiComplexV2JsonLdRequestParser( val filteredProperties = Seq( RDF.`type`.toString, Rdfs.Label, - KnoraApiV2Complex.AttachedToProject, - KnoraApiV2Complex.AttachedToUser, - KnoraApiV2Complex.HasPermissions, - KnoraApiV2Complex.CreationDate, + AttachedToProject, + AttachedToUser, + HasPermissions, + CreationDate, ) val valueStatements = r .listProperties() @@ -130,7 +261,7 @@ final case class ApiComplexV2JsonLdRequestParser( propertyIri <- valuePropertyIri(statement) customValueIri <- valueIri(valueResource) customValueUuid <- ZIO.fromEither(valueHasUuid(valueResource)) - customValueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource)) + customValueCreationDate <- instantOption(valueResource, ValueCreationDate) permissions <- ZIO.fromEither(valuePermissions(valueResource)) } yield ( propertyIri, @@ -143,12 +274,6 @@ final case class ApiComplexV2JsonLdRequestParser( ), ) - def creationDate(r: Resource): IO[String, Option[Instant]] = - for { - dateStr <- ZIO.fromEither(r.objectDataTypeOption(CreationDate, Xsd.DateTimeStamp)) - inst <- ZIO.foreach(dateStr)(str => ZIO.fromEither(ValuesValidator.parseXsdDateTimeStamp(str))) - } yield inst - def attachedToUser(r: Resource, requestingUser: User, projectIri: ProjectIri): IO[String, User] = for { userStr <- ZIO.fromEither(r.objectUriOption(AttachedToUser)) @@ -163,7 +288,7 @@ final case class ApiComplexV2JsonLdRequestParser( if !(requestingUser.permissions.isSystemAdmin || requestingUser.permissions.isProjectAdmin(projectIri.value)) => ZIO.fail( - s"You are logged in as ${requestingUser.username}, but only a system or project administrator can perform an operation as another user.", + s"You are logged in as ${requestingUser.username}, but only a system or project administrator can perform an operation as another user", ) case _ => userService.findUserByIri(userIri).orDie.someOrFail(s"User '${userIri.value}' not found") } @@ -193,29 +318,26 @@ final case class ApiComplexV2JsonLdRequestParser( def updateValueV2fromJsonLd(str: String, ingestState: AssetIngestState): IO[String, UpdateValueV2] = ZIO.scoped { for { - model <- ModelOps.fromJsonLd(str) - resourceAndIri <- resourceAndIri(model) - (resource, resourceIri) = resourceAndIri - resourceClassIri <- resourceClassIri(resource) - valueStatement <- valueStatement(resource) + r <- RootUriResource.fromJsonLd(str) + valueStatement <- valueStatement(r.resource) valuePropertyIri <- valuePropertyIri(valueStatement) valueResource = valueStatement.getObject.asResource() valueType <- valueType(valueResource) valueIri <- valueIri(valueResource).someOrFail("The value IRI is required") newValueVersionIri <- newValueVersionIri(valueResource, valueIri) - valueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource)) + valueCreationDate <- instantOption(valueResource, ValueCreationDate) valuePermissions <- ZIO.fromEither(valuePermissions(valueResource)) valueFileValueFilename <- ZIO.fromEither(valueFileValueFilename(valueResource)) valueContent <- - getValueContent(valueType.toString, valueResource, valueFileValueFilename, resourceIri.shortcode, ingestState) + getValueContent(valueType.toString, valueResource, valueFileValueFilename, r.shortcode, ingestState) .map(Some(_)) .orElse(ZIO.none) updateValue <- valueContent match case Some(valueContentV2) => ZIO.succeed( UpdateValueContentV2( - resourceIri.toString, - resourceClassIri.smartIri, + r.resourceIriStr, + r.resourceClassSmartIri, valuePropertyIri.smartIri, valueIri.toString, valueContentV2, @@ -232,8 +354,8 @@ final case class ApiComplexV2JsonLdRequestParser( _ => "No permissions and no value content found", permissions => UpdateValuePermissionsV2( - resourceIri.toString, - resourceClassIri.smartIri, + r.resourceIriStr, + r.resourceClassSmartIri, valuePropertyIri.smartIri, valueIri.toString, valueType, @@ -248,24 +370,21 @@ final case class ApiComplexV2JsonLdRequestParser( def createValueV2FromJsonLd(str: String, ingestState: AssetIngestState): IO[String, CreateValueV2] = ZIO.scoped { for { - model <- ModelOps.fromJsonLd(str) - resourceAndIri <- resourceAndIri(model) - (resource, resourceIri) = resourceAndIri - resourceClassIri <- resourceClassIri(resource) - valueStatement <- valueStatement(resource) + r <- RootUriResource.fromJsonLd(str) + valueStatement <- valueStatement(r.resource) valuePropertyIri <- valuePropertyIri(valueStatement) valueResource <- ZIO.fromEither(valueStatement.objectAsResource()) valueIri <- valueIri(valueResource) valueUuid <- ZIO.fromEither(valueHasUuid(valueResource)) - valueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource)) + valueCreationDate <- instantOption(valueResource, ValueCreationDate) valuePermissions <- ZIO.fromEither(valuePermissions(valueResource)) valueFileValueFilename <- ZIO.fromEither(valueFileValueFilename(valueResource)) valueType <- valueType(valueResource) valueContent <- - getValueContent(valueType.toString, valueResource, valueFileValueFilename, resourceIri.shortcode, ingestState) + getValueContent(valueType.toString, valueResource, valueFileValueFilename, r.shortcode, ingestState) } yield CreateValueV2( - resourceIri.toString, - resourceClassIri.smartIri, + r.resourceIriStr, + r.resourceClassSmartIri, valuePropertyIri.smartIri, valueContent, valueIri.map(_.smartIri), @@ -276,21 +395,6 @@ final case class ApiComplexV2JsonLdRequestParser( ) } - private def resourceAndIri(model: Model): IO[String, (Resource, ResourceIri)] = - resourceAndIriOption(model).flatMap { - case (r, Some(iri)) => ZIO.succeed((r, iri)) - case (r, None) => ZIO.fail("No resource IRI found") - } - - private def resourceAndIriOption(model: Model): IO[String, (Resource, Option[KResourceIri])] = - ZIO.fromEither(model.singleRootResource).flatMap { (r: Resource) => - ZIO - .foreach(r.uri)( - converter.asSmartIri(_).mapError(_.getMessage).flatMap(iri => ZIO.fromEither(KResourceIri.from(iri))), - ) - .map((r, _)) - } - private def valueStatement(rootResource: Resource): IO[String, Statement] = ZIO .succeed(rootResource.listProperties().asScala.filter(_.getPredicate != RDF.`type`).toList) .filterOrFail(_.nonEmpty)("No value property found in root resource") @@ -301,7 +405,7 @@ final case class ApiComplexV2JsonLdRequestParser( converter .asSmartIri(valueStatement.predicateUri) .mapError(_.getMessage) - .flatMap(iri => ZIO.fromEither(PropertyIri.from(iri))) + .flatMap(iri => ZIO.fromEither(PropertyIri.fromApiV2Complex(iri))) private def valueType(resource: Resource) = ZIO .fromEither(resource.rdfsType.toRight("No rdf:type found for value.")) @@ -321,24 +425,12 @@ final case class ApiComplexV2JsonLdRequestParser( case None => Right(None) } - private def valueCreationDate(valueResource: Resource): Either[String, Option[Instant]] = - valueResource.objectDataTypeOption(ValueCreationDate, Xsd.DateTimeStamp).flatMap { - case Some(str) => ValuesValidator.parseXsdDateTimeStamp(str).map(Some(_)) - case None => Right(None) - } - private def valuePermissions(valueResource: Resource): Either[String, Option[String]] = valueResource.objectStringOption(HasPermissions) private def valueFileValueFilename(valueResource: Resource): Either[String, Option[String]] = valueResource.objectStringOption(FileValueHasFilename) - private def resourceClassIri(rootResource: Resource): IO[String, KResourceClassIri] = ZIO - .fromOption(rootResource.rdfsType) - .orElseFail("No root resource class IRI found") - .flatMap(converter.asSmartIri(_).mapError(_.getMessage)) - .flatMap(iri => ZIO.fromEither(KResourceClassIri.from(iri))) - private def getValueContent( valueType: String, valueResource: Resource,