Skip to content

Commit

Permalink
feat: Fully support json-ld for delete value and update, delete or er…
Browse files Browse the repository at this point in the history
…ase resource (#3422)
  • Loading branch information
seakayone authored Nov 13, 2024
1 parent 419c74d commit e79a2f9
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 332 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

package org.knora.webapi.messages.v2.responder.resourcemessages

import zio.*

import java.time.Instant
import java.util.UUID

Expand All @@ -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
Expand All @@ -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`.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.*
Expand Down Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit e79a2f9

Please sign in to comment.