From 039dcb655f37811b76b58a6a4bc9b28d47047686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 11 Nov 2024 17:15:18 +0100 Subject: [PATCH] feat: Support propert json-ld for update value endpoint (DEV-4325) (#3418) --- .../webapi/e2e/v2/ValuesRouteV2E2ESpec.scala | 2 +- .../valuemessages/ValueMessagesV2.scala | 172 ------------------ .../webapi/routing/v2/ValuesRouteV2.scala | 4 +- .../ApiComplexV2JsonLdRequestParser.scala | 72 ++++++++ .../knora/webapi/slice/common/KnoraIris.scala | 5 +- .../slice/common/jena/ModelOpsSpec.scala | 9 + 6 files changed, 89 insertions(+), 175 deletions(-) diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala index e8f7b2b9f6..8caac18595 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala @@ -3066,7 +3066,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { "not update an integer value with an invalid custom new value version IRI" in { val resourceIri: IRI = AThing.iri val intValue: Int = 8 - val newValueVersionIri: IRI = "foo" + val newValueVersionIri: IRI = "http://example.com/foo" val jsonLDEntity = updateIntValueWithCustomNewValueVersionIriRequest( resourceIri = resourceIri, 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 23aee9e900..ceb863af0b 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 @@ -594,178 +594,6 @@ sealed trait UpdateValueV2 { val valueCreationDate: Option[Instant] } -object UpdateValueV2 { - - /** - * Converts JSON-LD input to a [[UpdateValueV2]]. - * - * @param jsonLdString the JSON-LD input as String. - * @param requestingUser the user making the request. - * @return a case class instance representing the input. - */ - def fromJsonLd( - ingestState: AssetIngestState, - jsonLdString: String, - requestingUser: User, - ): ZIO[IriConverter & SipiService & StringFormatter & MessageRelay, Throwable, UpdateValueV2] = - ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => - def makeUpdateValueContentV2( - resourceIri: SmartIri, - resourceClassIri: SmartIri, - propertyIri: SmartIri, - jsonLDObject: JsonLDObject, - valueIri: SmartIri, - maybeValueCreationDate: Option[Instant], - maybeNewIri: Option[SmartIri], - ) = - for { - maybePermissions <- - ZIO.attempt { - val validationFun: (String, => Nothing) => String = - (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) - jsonLDObject.maybeStringWithValidation(HasPermissions, validationFun) - } - shortcode <- ZIO - .fromEither(resourceIri.getProjectShortcode) - .mapError(msg => NotFoundException(s"Shortcode not found. $msg")) - fileInfo <- ValueContentV2.getFileInfo(shortcode, ingestState, jsonLDObject) - valueContent <- ValueContentV2.fromJsonLdObject(jsonLDObject, requestingUser, fileInfo) - } yield UpdateValueContentV2( - resourceIri = resourceIri.toString, - resourceClassIri = resourceClassIri, - propertyIri = propertyIri, - valueIri = valueIri.toString, - valueContent = valueContent, - permissions = maybePermissions, - valueCreationDate = maybeValueCreationDate, - newValueVersionIri = maybeNewIri, - ingestState = ingestState, - ) - - def makeUpdateValuePermissionsV2( - resourceIri: SmartIri, - resourceClassIri: SmartIri, - propertyIri: SmartIri, - jsonLDObject: JsonLDObject, - valueIri: SmartIri, - maybeValueCreationDate: Option[Instant], - maybeNewIri: Option[SmartIri], - ) = ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => - // Yes. This is a request to change the value's permissions. - for { - valueType <- ZIO.attempt( - jsonLDObject.requireStringWithValidation( - JsonLDKeywords.TYPE, - stringFormatter.toSmartIriWithErr, - ), - ) - permissions <- ZIO.attempt { - val validationFun: (String, => Nothing) => String = - (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) - jsonLDObject.requireStringWithValidation(HasPermissions, validationFun) - } - } yield UpdateValuePermissionsV2( - resourceIri = resourceIri.toString, - resourceClassIri = resourceClassIri, - propertyIri = propertyIri, - valueIri = valueIri.toString, - valueType = valueType, - permissions = permissions, - valueCreationDate = maybeValueCreationDate, - newValueVersionIri = maybeNewIri, - ) - } - - for { - jsonLdDocument <- RouteUtilV2.parseJsonLd(jsonLdString) - // Get the IRI of the resource that the value is to be created in. - resourceIri <- jsonLdDocument.body.getRequiredIdValueAsKnoraDataIri - .mapError(BadRequestException(_)) - .flatMap(RouteUtilZ.ensureIsKnoraResourceIri) - // Get the resource class. - resourceClassIri <- - jsonLdDocument.body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_)) - - // Get the resource property and the new value version. - updateValue <- - jsonLdDocument.body.getRequiredResourcePropertyApiV2ComplexValue.mapError(BadRequestException(_)).flatMap { - case (propertyIri: SmartIri, jsonLDObject: JsonLDObject) => - // Get the custom value creation date, if provided. - - for { - valueIri <- jsonLDObject.getRequiredIdValueAsKnoraDataIri.mapError(BadRequestException(_)) - // Aside from the value's ID and type and the optional predicates above, does the value object just - otherValuePredicates: Set[IRI] = jsonLDObject.value.keySet -- Set( - JsonLDKeywords.ID, - JsonLDKeywords.TYPE, - ValueCreationDate, - NewValueVersionIri, - ) - maybeValueCreationDate <- ZIO.attempt( - jsonLDObject.maybeDatatypeValueInObject( - key = ValueCreationDate, - expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, - validationFun = (s, errorFun) => - ValuesValidator - .xsdDateTimeStampToInstant(s) - .getOrElse(errorFun), - ), - ) - // Get and validate the custom new value version IRI, if provided. - - maybeNewIri <- - ZIO - .attempt( - jsonLDObject - .maybeIriInObject(NewValueVersionIri, stringFormatter.toSmartIriWithErr), - ) - .flatMap(smartIriMaybe => - ZIO.foreach(smartIriMaybe) { definedNewIri => - if (definedNewIri == valueIri) { - ZIO.fail( - BadRequestException( - s"The IRI of a new value version cannot be the same as the IRI of the current version", - ), - ) - } else { - ZIO.attempt( - stringFormatter.validateCustomValueIri( - customValueIri = definedNewIri, - projectCode = valueIri.getProjectCode.get, - resourceID = valueIri.getResourceID.get, - ), - ) - } - }, - ) - - value <- if (otherValuePredicates == Set(HasPermissions)) { - makeUpdateValuePermissionsV2( - resourceIri, - resourceClassIri, - propertyIri, - jsonLDObject, - valueIri, - maybeValueCreationDate, - maybeNewIri, - ) - } else { - makeUpdateValueContentV2( - resourceIri, - resourceClassIri, - propertyIri, - jsonLDObject, - valueIri, - maybeValueCreationDate, - maybeNewIri, - ) - } - } yield value - } - } yield updateValue - } -} - /** * A new version of a value of a Knora property to be created. * 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 dee6c6f8af..abee80b6d2 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 @@ -108,7 +108,9 @@ final case class ValuesRouteV2()( requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(ctx)) apiRequestId <- Random.nextUUID ingestState = AssetIngestState.headerAssetIngestState(ctx.request.headers) - updateValue <- UpdateValueV2.fromJsonLd(ingestState, jsonLdString, requestingUser) + updateValue <- ZIO.serviceWithZIO[ApiComplexV2JsonLdRequestParser]( + _.updateValueV2fromJsonLd(jsonLdString, ingestState).mapError(BadRequestException(_)), + ) response <- ZIO.serviceWithZIO[ValuesResponderV2](_.updateValueV2(updateValue, requestingUser, apiRequestId)) } yield response, 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 cec10fe54c..1656b9d96e 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 @@ -20,6 +20,7 @@ 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.Xsd +import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo @@ -43,6 +44,77 @@ final case class ApiComplexV2JsonLdRequestParser( sipiService: SipiService, ) { + 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) + valuePropertyIri <- valuePropertyIri(valueStatement) + valueType <- valueType(valueStatement) + valueResource = valueStatement.getObject.asResource() + valueIri <- valueIri(valueResource).someOrFail("The value IRI is required") + newValueVersionIri <- newValueVersionIri(valueResource, valueIri) + valueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource)) + valuePermissions <- ZIO.fromEither(valuePermissions(valueResource)) + valueFileValueFilename <- ZIO.fromEither(valueFileValueFilename(valueResource)) + valueContent <- + getValueContent(valueType.toString, valueResource, valueFileValueFilename, resourceIri.shortcode, ingestState) + .map(Some(_)) + .orElse(ZIO.none) + updateValue <- valueContent match + case Some(valueContentV2) => + ZIO.succeed( + UpdateValueContentV2( + resourceIri.toString, + resourceClassIri.smartIri, + valuePropertyIri.smartIri, + valueIri.toString, + valueContentV2, + valuePermissions, + valueCreationDate, + newValueVersionIri.map(_.smartIri), + ingestState, + ), + ) + case None => + ZIO + .fromOption(valuePermissions) + .mapBoth( + _ => "No permissions and no value content found", + permissions => + UpdateValuePermissionsV2( + resourceIri.toString, + resourceClassIri.smartIri, + valuePropertyIri.smartIri, + valueIri.toString, + valueType, + permissions, + valueCreationDate, + newValueVersionIri.map(_.smartIri), + ), + ) + } yield updateValue + } + + private def newValueVersionIri(r: Resource, valueIri: ValueIri): IO[String, Option[ValueIri]] = + ZIO + .fromEither(r.objectUriOption(NewValueVersionIri)) + .some + .flatMap(converter.asSmartIri(_).mapError(_.getMessage).asSomeError) + .flatMap(iri => ZIO.fromEither(ValueIri.from(iri)).asSomeError) + .filterOrFail(newV => newV != valueIri)( + Some(s"The IRI of a new value version cannot be the same as the IRI of the current version"), + ) + .filterOrFail(newV => newV.sameResourceAs(valueIri))( + Some( + s"The project shortcode and resource must be equal for the new value version and the current version", + ), + ) + .unsome + def createValueV2FromJsonLd(str: String, ingestState: AssetIngestState): IO[String, CreateValueV2] = ZIO.scoped { for { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala index 433b579033..9cb95d9b5e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala @@ -53,7 +53,10 @@ object KnoraIris { shortcode: Shortcode, resourceId: ResourceId, valueId: ValueId, - ) extends KnoraIri + ) extends KnoraIri { + def sameResourceAs(other: ValueIri): Boolean = + this.shortcode == other.shortcode && this.resourceId == other.resourceId + } final case class ResourceClassIri private (smartIri: SmartIri, entityName: EntityName) extends KnoraIri diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/jena/ModelOpsSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/jena/ModelOpsSpec.scala index 926b4c4885..cbe90d1fca 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/common/jena/ModelOpsSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/common/jena/ModelOpsSpec.scala @@ -53,6 +53,15 @@ object ModelOpsSpec extends ZIOSpecDefault { model2 <- ModelOps.fromJsonLd(jsonLd2) } yield assertTrue(model1.isIsomorphicWith(model2)) }, + test("should fail on invalid json ld") { + for { + exit <- ModelOps.fromJsonLd("invalid json ld").exit + } yield assertTrue( + exit == Exit.Failure( + Cause.fail("[line: 1, col: 1 ] The document could not be loaded or parsed [code=LOADING_DOCUMENT_FAILED]."), + ), + ) + }, ), ) }