diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala
index 5e6425e288..d3d49aebf2 100644
--- a/integration/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala
+++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala
@@ -1344,7 +1344,7 @@ class ResourcesRouteV2E2ESpec extends E2ESpec {
HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity),
) ~> addCredentials(BasicHttpCredentials(SharedTestDataADM.anythingUser2.email, password))
val response = singleAwaitingRequest(request)
- assert(response.status == StatusCodes.Forbidden, "should be forbidden")
+ assert(response.status == StatusCodes.BadRequest)
}
"create a resource containing escaped text" in {
diff --git a/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala b/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala
index eef452bcbf..679587562d 100644
--- a/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala
+++ b/integration/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala
@@ -449,7 +449,7 @@ class KnoraSipiIntegrationV2ITSpec
val request = Post(s"$baseApiUrl/v2/resources", jsonLdHttpEntity(jsonLdEntity)) ~> addAuthorization
val response = singleAwaitingRequest(request)
- assert(response.status == StatusCodes.NotFound)
+ assert(response.status == StatusCodes.BadRequest)
val body = Await.result(Unmarshal(response.entity).to[String], 1.seconds)
assert(
body.contains(
diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/UserUtilADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/UserUtilADM.scala
deleted file mode 100644
index 8a9ec20677..0000000000
--- a/webapi/src/main/scala/org/knora/webapi/messages/util/UserUtilADM.scala
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright © 2021 - 2024 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.util
-
-import zio.*
-
-import dsp.errors.ForbiddenException
-import dsp.errors.NotFoundException
-import org.knora.webapi.IRI
-import org.knora.webapi.slice.admin.domain.model.User
-import org.knora.webapi.slice.admin.domain.model.UserIri
-import org.knora.webapi.slice.admin.domain.service.UserService
-
-/**
- * Utility functions for working with users.
- */
-object UserUtilADM {
-
- /**
- * Allows a system admin or project admin to perform an operation as another user in a specified project.
- * Checks whether the requesting user is a system admin or a project admin in the project, and if so,
- * returns a [[User]] representing the requested user. Otherwise, returns a failed future containing
- * [[ForbiddenException]].
- *
- * @param requestingUser the requesting user.
- * @param requestedUserIri the IRI of the requested user.
- * @param projectIri the IRI of the project.
- * @return a [[User]] representing the requested user.
- */
- def switchToUser(
- requestingUser: User,
- requestedUserIri: IRI,
- projectIri: IRI,
- ): ZIO[UserService, Throwable, User] = {
- val userIri = UserIri.unsafeFrom(requestedUserIri)
- requestingUser match {
- case _ if requestingUser.id == userIri.value => ZIO.succeed(requestingUser)
- case _ if !(requestingUser.permissions.isSystemAdmin || requestingUser.permissions.isProjectAdmin(projectIri)) =>
- val msg =
- s"You are logged in as ${requestingUser.username}, but only a system administrator or project administrator can perform an operation as another user"
- ZIO.fail(ForbiddenException(msg))
- case _ =>
- ZIO.serviceWithZIO[UserService](
- _.findUserByIri(userIri).someOrFail(NotFoundException(s"User '${userIri.value}' not found")),
- )
- }
- }
-}
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 55038f4ac1..44bc97920f 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
@@ -15,7 +15,6 @@ import dsp.valueobjects.Iri
import dsp.valueobjects.UuidUtil
import org.knora.webapi.*
import org.knora.webapi.config.AppConfig
-import org.knora.webapi.core.MessageRelay
import org.knora.webapi.core.RelayedMessage
import org.knora.webapi.messages.IriConversions.*
import org.knora.webapi.messages.OntologyConstants.*
@@ -36,11 +35,7 @@ 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.admin.domain.service.ProjectService
-import org.knora.webapi.slice.admin.domain.service.UserService
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
-import org.knora.webapi.store.iiif.api.SipiService
-import org.knora.webapi.util.*
/**
* An abstract trait for messages that can be sent to `ResourcesResponderV2`.
@@ -651,201 +646,6 @@ case class CreateResourceRequestV2(
ingestState: AssetIngestState = AssetInTemp,
) extends ResourcesResponderRequestV2
-object CreateResourceRequestV2 {
-
- /**
- * Converts JSON-LD input to a [[CreateResourceRequestV2]].
- *
- * @param jsonLDDocument the JSON-LD input.
- * @param apiRequestID the UUID of the API request.
- * @param requestingUser the user making the request.
- * @param ingestState indicates the state of the file, either ingested or in temp folder
- * @return a case class instance representing the input.
- */
- def fromJsonLd(
- jsonLDDocument: JsonLDDocument,
- apiRequestID: UUID,
- requestingUser: User,
- ingestState: AssetIngestState = AssetInTemp,
- ): ZIO[
- IriConverter & MessageRelay & ProjectService & SipiService & StringFormatter & UserService,
- Throwable,
- CreateResourceRequestV2,
- ] =
- ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter =>
- val validationFun: (String, => Nothing) => String =
- (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun)
- for {
- // Get the resource class.
- resourceClassIri <-
- jsonLDDocument.body.getRequiredTypeAsKnoraApiV2ComplexTypeIri.mapError(BadRequestException(_))
-
- // Get the custom resource IRI if provided.
- maybeCustomResourceIri <- jsonLDDocument.body.getIdValueAsKnoraDataIri.mapError(BadRequestException(_))
-
- // Get the resource's rdfs:label.
- label <-
- ZIO.attempt(jsonLDDocument.body.requireStringWithValidation(Rdfs.Label, (s, _) => s))
-
- // Get information about the project that the resource should be created in.
- projectIri <- ZIO.attempt(
- jsonLDDocument.body.requireIriInObject(
- KnoraApiV2Complex.AttachedToProject,
- stringFormatter.toSmartIriWithErr,
- ),
- )
- projectId <- ZIO.fromEither(ProjectIri.from(projectIri.toString)).mapError(BadRequestException.apply)
- project <- ZIO
- .serviceWithZIO[ProjectService](_.findById(projectId))
- .someOrFail(NotFoundException(s"Project '$projectIri' not found"))
-
- _ <- ZIO.attempt(maybeCustomResourceIri.foreach { iri =>
- if (!iri.isKnoraResourceIri) {
- throw BadRequestException(s"<$iri> is not a Knora resource IRI")
- }
-
- if (!iri.getProjectCode.contains(project.shortcode)) {
- throw BadRequestException(s"The provided resource IRI does not contain the correct project code")
- }
- })
-
- // Get the resource's permissions.
- permissions <- ZIO.attempt(
- jsonLDDocument.body
- .maybeStringWithValidation(KnoraApiV2Complex.HasPermissions, validationFun),
- )
-
- // Get the user who should be indicated as the creator of the resource, if specified.
-
- maybeAttachedToUserIri <- ZIO.attempt(
- jsonLDDocument.body.maybeIriInObject(
- KnoraApiV2Complex.AttachedToUser,
- stringFormatter.toSmartIriWithErr,
- ),
- )
-
- maybeAttachedToUser <- ZIO.foreach(maybeAttachedToUserIri) { attachedToUserIri =>
- UserUtilADM.switchToUser(
- requestingUser,
- attachedToUserIri.toString,
- projectIri.toString,
- )
- }
-
- creationDate <- ZIO.attempt(
- jsonLDDocument.body.maybeDatatypeValueInObject(
- key = KnoraApiV2Complex.CreationDate,
- expectedDatatype = Xsd.DateTimeStamp.toSmartIri,
- validationFun = (s, errorFun) => xsdDateTimeStampToInstant(s).getOrElse(errorFun),
- ),
- )
- // Get the resource's values.
-
- propertyIriStrs <- ZIO.attempt(
- jsonLDDocument.body.value.keySet --
- Set(
- JsonLDKeywords.ID,
- JsonLDKeywords.TYPE,
- Rdfs.Label,
- KnoraApiV2Complex.AttachedToProject,
- KnoraApiV2Complex.AttachedToUser,
- KnoraApiV2Complex.HasPermissions,
- KnoraApiV2Complex.CreationDate,
- ),
- )
-
- propertyValueFuturesMap <-
- ZIO.attempt(
- propertyIriStrs.map { propertyIriStr =>
- val propertyIri: SmartIri =
- propertyIriStr.toSmartIriWithErr(throw BadRequestException(s"Invalid property IRI: <$propertyIriStr>"))
- val valuesArray: JsonLDArray = jsonLDDocument.body
- .getRequiredArray(propertyIriStr)
- .fold(e => throw BadRequestException(e), identity)
-
- val valueFuturesSeq = ZIO.foreach(valuesArray.value) { valueJsonLD =>
- for {
- valueJsonLDObject <- valueJsonLD match {
- case jsonLDObject: JsonLDObject => ZIO.succeed(jsonLDObject)
- case _ =>
- ZIO.fail(
- BadRequestException(
- s"Invalid JSON-LD as object of property <$propertyIriStr>",
- ),
- )
- }
- fileInfo <- ValueContentV2.getFileInfo(project.getShortcode, ingestState, valueJsonLDObject)
- valueContent <- ValueContentV2.fromJsonLdObject(valueJsonLDObject, requestingUser, fileInfo)
-
- maybeCustomValueIri <- valueJsonLDObject.getIdValueAsKnoraDataIri
- .mapError(BadRequestException(_))
- maybeCustomValueUUID <- valueJsonLDObject
- .getUuid(KnoraApiV2Complex.ValueHasUUID)
- .mapError(BadRequestException(_))
-
- // Get the value's creation date.
- // TODO: creationDate for values is a bug, and will not be supported in future. Use valueCreationDate instead.
- maybeCustomValueCreationDate <- ZIO
- .attempt(
- valueJsonLDObject
- .maybeDatatypeValueInObject(
- key = KnoraApiV2Complex.ValueCreationDate,
- expectedDatatype = Xsd.DateTimeStamp.toSmartIri,
- validationFun = (s, errorFun) =>
- xsdDateTimeStampToInstant(s)
- .getOrElse(errorFun),
- ),
- )
- .flatMap(it =>
- if (it.isEmpty) {
- ZIO.attempt(
- valueJsonLDObject.maybeDatatypeValueInObject(
- key = KnoraApiV2Complex.CreationDate,
- expectedDatatype = Xsd.DateTimeStamp.toSmartIri,
- validationFun = (s, errorFun) =>
- xsdDateTimeStampToInstant(s)
- .getOrElse(errorFun),
- ),
- )
- } else ZIO.succeed(it),
- )
-
- maybePermissions <- ZIO.attempt(
- valueJsonLDObject.maybeStringWithValidation(
- KnoraApiV2Complex.HasPermissions,
- validationFun,
- ),
- )
- } yield CreateValueInNewResourceV2(
- valueContent = valueContent,
- customValueIri = maybeCustomValueIri,
- customValueUUID = maybeCustomValueUUID,
- customValueCreationDate = maybeCustomValueCreationDate,
- permissions = maybePermissions,
- )
- }
-
- propertyIri -> valueFuturesSeq
- }.toMap,
- )
- propertyValuesMap <- ZioHelper.sequence(propertyValueFuturesMap)
- } yield CreateResourceRequestV2(
- createResource = CreateResourceV2(
- resourceIri = maybeCustomResourceIri,
- resourceClassIri = resourceClassIri,
- label = label,
- values = propertyValuesMap,
- projectADM = project,
- permissions = permissions,
- creationDate = creationDate,
- ),
- requestingUser = maybeAttachedToUser.getOrElse(requestingUser),
- apiRequestID = apiRequestID,
- ingestState = ingestState,
- )
- }
-}
-
/**
* Represents a request to update a resource's metadata.
*
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 ceb863af0b..8f33357ead 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
@@ -18,7 +18,6 @@ import scala.util.Try
import dsp.errors.AssertionException
import dsp.errors.BadRequestException
import dsp.errors.NotFoundException
-import dsp.errors.NotImplementedException
import dsp.valueobjects.Iri
import dsp.valueobjects.IriErrorMessages
import dsp.valueobjects.UuidUtil
@@ -49,7 +48,6 @@ import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId
import org.knora.webapi.slice.admin.api.model.Project
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode
import org.knora.webapi.slice.admin.domain.model.Permission
-import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.common.jena.JenaConversions.given
import org.knora.webapi.slice.common.jena.ResourceOps.*
import org.knora.webapi.slice.resourceinfo.domain.InternalIri
@@ -738,80 +736,6 @@ sealed trait ValueContentV2 extends KnoraContentV2[ValueContentV2] with WithAsIs
*/
object ValueContentV2 {
- /**
- * Converts a JSON-LD object to a [[ValueContentV2]].
- *
- * @param jsonLdObject the JSON-LD object.
- * @param requestingUser the user making the request.
- * @return a [[ValueContentV2]].
- */
- def fromJsonLdObject(
- jsonLdObject: JsonLDObject,
- requestingUser: User,
- fileInfo: Option[FileInfo],
- ): ZIO[StringFormatter & MessageRelay, Throwable, ValueContentV2] =
- ZIO.serviceWithZIO[StringFormatter] { stringFormatter =>
- for {
- valueType <-
- ZIO.attempt(jsonLdObject.requireStringWithValidation(JsonLDKeywords.TYPE, stringFormatter.toSmartIriWithErr))
-
- valueContent <-
- valueType.toString match {
- case TextValue => TextValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser)
- case IntValue => IntegerValueContentV2.fromJsonLdObject(jsonLdObject)
- case DecimalValue => DecimalValueContentV2.fromJsonLdObject(jsonLdObject)
- case BooleanValue => BooleanValueContentV2.fromJsonLdObject(jsonLdObject)
- case KnoraApiV2Complex.DateValue => DateValueContentV2.fromJsonLdObject(jsonLdObject)
- case GeomValue => GeomValueContentV2.fromJsonLdObject(jsonLdObject)
- case IntervalValue => IntervalValueContentV2.fromJsonLdObject(jsonLdObject)
- case TimeValue => TimeValueContentV2.fromJsonLdObject(jsonLdObject)
- case LinkValue => LinkValueContentV2.fromJsonLdObject(jsonLdObject)
- case ListValue => HierarchicalListValueContentV2.fromJsonLdObject(jsonLdObject)
- case UriValue => UriValueContentV2.fromJsonLdObject(jsonLdObject)
- case GeonameValue => GeonameValueContentV2.fromJsonLdObject(jsonLdObject)
- case ColorValue => ColorValueContentV2.fromJsonLdObject(jsonLdObject)
- case StillImageFileValue =>
- for {
- info <-
- ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for StillImageFileValue"))
- content <- StillImageFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata)
- } yield content
- case StillImageExternalFileValue => StillImageExternalFileValueContentV2.fromJsonLdObject(jsonLdObject)
- case DocumentFileValue =>
- for {
- info <-
- ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for DocumentFileValue"))
- content <- DocumentFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata)
- } yield content
- case TextFileValue =>
- for {
- info <- ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for TextFileValue"))
- content <- TextFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata)
- } yield content
- case AudioFileValue =>
- for {
- info <-
- ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for AudioFileValue"))
- content <- AudioFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata)
- } yield content
- case MovingImageFileValue =>
- for {
- info <- ZIO
- .fromOption(fileInfo)
- .orElseFail(BadRequestException("No file info found for MovingImageFileValue"))
- content <- MovingImageFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata)
- } yield content
- case ArchiveFileValue =>
- for {
- info <-
- ZIO.fromOption(fileInfo).orElseFail(BadRequestException("No file info found for ArchiveFileValue"))
- content <- ArchiveFileValueContentV2.fromJsonLdObject(jsonLdObject, info.filename, info.metadata)
- } yield content
- case other => ZIO.fail(NotImplementedException(s"Parsing of JSON-LD value type not implemented: $other"))
- }
- } yield valueContent
- }
-
final case class FileInfo(filename: IRI, metadata: FileMetadataSipiResponse)
/**
@@ -1013,84 +937,6 @@ object DateValueContentV2 {
)
}
- /**
- * Converts a JSON-LD object to a [[DateValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return a [[DateValueContentV2]].
- */
- def fromJsonLdObject(jsonLDObject: JsonLDObject): ZIO[StringFormatter, Throwable, DateValueContentV2] =
- for {
- comment <- JsonLDUtil.getComment(jsonLDObject)
- calendarName <- ZIO.attempt(jsonLDObject.requireStringWithValidation(DateValueHasCalendar, CalendarNameV2.parse))
- dateValueHasStartYear <- ZIO
- .fromEither(jsonLDObject.getRequiredInt(DateValueHasStartYear))
- .mapError(BadRequestException(_))
- maybeDateValueHasStartMonth <- ZIO
- .fromEither(jsonLDObject.getInt(DateValueHasStartMonth))
- .mapError(BadRequestException(_))
- maybeDateValueHasStartDay <- ZIO
- .fromEither(jsonLDObject.getInt(DateValueHasStartDay))
- .mapError(BadRequestException(_))
- maybeDateValueHasStartEra <-
- ZIO.attempt(jsonLDObject.maybeStringWithValidation(DateValueHasStartEra, DateEraV2.parse))
- dateValueHasEndYear <- ZIO
- .fromEither(jsonLDObject.getRequiredInt(DateValueHasEndYear))
- .mapError(BadRequestException(_))
- maybeDateValueHasEndMonth <- ZIO
- .fromEither(jsonLDObject.getInt(DateValueHasEndMonth))
- .mapError(BadRequestException(_))
- maybeDateValueHasEndDay <- ZIO
- .fromEither(jsonLDObject.getInt(DateValueHasEndDay))
- .mapError(BadRequestException(_))
- maybeDateValueHasEndEra <-
- ZIO.attempt(jsonLDObject.maybeStringWithValidation(DateValueHasEndEra, DateEraV2.parse))
- _ <- ZIO
- .fail(AssertionException(s"Invalid date: $jsonLDObject"))
- .when(maybeDateValueHasStartMonth.isEmpty && maybeDateValueHasStartDay.isDefined)
- _ <- ZIO
- .fail(AssertionException(s"Invalid date: $jsonLDObject"))
- .when(maybeDateValueHasEndMonth.isEmpty && maybeDateValueHasEndDay.isDefined)
- // Check that the era is given if required.
- _ <- ZIO
- .fail(AssertionException(s"Era is required in calendar $calendarName"))
- .when(
- calendarName.isInstanceOf[CalendarNameGregorianOrJulian] &&
- (maybeDateValueHasStartEra.isEmpty || maybeDateValueHasEndEra.isEmpty),
- )
-
- // Construct a CalendarDateRangeV2 representing the start and end dates.
- startCalendarDate = CalendarDateV2(
- calendarName = calendarName,
- year = dateValueHasStartYear,
- maybeMonth = maybeDateValueHasStartMonth,
- maybeDay = maybeDateValueHasStartDay,
- maybeEra = maybeDateValueHasStartEra,
- )
-
- endCalendarDate = CalendarDateV2(
- calendarName = calendarName,
- year = dateValueHasEndYear,
- maybeMonth = maybeDateValueHasEndMonth,
- maybeDay = maybeDateValueHasEndDay,
- maybeEra = maybeDateValueHasEndEra,
- )
-
- dateRange = CalendarDateRangeV2(startCalendarDate, endCalendarDate)
-
- // Convert the CalendarDateRangeV2 to start and end Julian Day Numbers.
- (startJDN, endJDN) = dateRange.toJulianDayRange
-
- } yield DateValueContentV2(
- ontologySchema = ApiV2Complex,
- valueHasStartJDN = startJDN,
- valueHasEndJDN = endJDN,
- valueHasStartPrecision = startCalendarDate.precision,
- valueHasEndPrecision = endCalendarDate.precision,
- valueHasCalendar = calendarName,
- comment,
- )
-
def from(r: Resource): Either[String, DateValueContentV2] = {
def objectEraOption(resource: Resource, property: String) = for {
eraStr <- resource.objectStringOption(property)
@@ -1440,21 +1286,6 @@ case class TextValueContentV2(
* Constructs [[TextValueContentV2]] objects based on JSON-LD input.
*/
object TextValueContentV2 {
- private def getSparqlEncodedString(
- obj: JsonLDObject,
- key: String,
- ): ZIO[StringFormatter, BadRequestException, Option[IRI]] =
- ZIO
- .fromEither(obj.getString(key))
- .mapError(BadRequestException(_))
- .flatMap(ZIO.foreach(_)(it => RouteUtilZ.toSparqlEncodedString(it, s"Invalid key: $key: $it")))
-
- private def getIriFromObject(obj: JsonLDObject, key: String): ZIO[StringFormatter, BadRequestException, Option[IRI]] =
- obj
- .getIriInObject(key)
- .mapError(BadRequestException(_))
- .flatMap(ZIO.foreach(_)(it => RouteUtilZ.validateAndEscapeIri(it, s"Invalid key: $key: $it")))
-
private def getTextValue(
maybeValueAsString: Option[IRI],
maybeTextValueAsXml: Option[String],
@@ -1510,37 +1341,6 @@ object TextValueContentV2 {
)
}
- /**
- * Converts a JSON-LD object to a [[TextValueContentV2]].
- *
- * @param jsonLdObject the JSON-LD object.
- * @param requestingUser the user making the request.
- * @return a [[TextValueContentV2]].
- */
- def fromJsonLdObject(
- jsonLdObject: JsonLDObject,
- requestingUser: User,
- ): ZIO[StringFormatter & MessageRelay, Throwable, TextValueContentV2] =
- for {
- maybeValueAsString <- getSparqlEncodedString(jsonLdObject, ValueAsString)
- maybeValueHasLanguage <- getSparqlEncodedString(jsonLdObject, TextValueHasLanguage)
- maybeTextValueAsXml <- ZIO.fromEither(jsonLdObject.getString(TextValueAsXml)).mapError(BadRequestException(_))
-
- // If the client supplied the IRI of a standoff-to-XML mapping, get the mapping.
- maybeMappingResponse <-
- getIriFromObject(jsonLdObject, TextValueHasMapping).flatMap(mappingIriOption =>
- ZIO.foreach(mappingIriOption) { mappingIri =>
- ZIO.serviceWithZIO[MessageRelay](_.ask[GetMappingResponseV2](GetMappingRequestV2(mappingIri)))
- },
- )
-
- comment <- JsonLDUtil.getComment(jsonLdObject)
- // Did the client submit text with or without standoff markup?
- textValue <-
- getTextValue(maybeValueAsString, maybeTextValueAsXml, maybeValueHasLanguage, maybeMappingResponse, comment)
-
- } yield textValue
-
private def objectSparqlStringOption(r: Resource, property: String) = for {
str <- r.objectStringOption(property)
iri <- str match
@@ -1622,23 +1422,6 @@ case class IntegerValueContentV2(ontologySchema: OntologySchema, valueHasInteger
* Constructs [[IntegerValueContentV2]] objects based on JSON-LD input.
*/
object IntegerValueContentV2 {
-
- /**
- * Converts a JSON-LD object to an [[IntegerValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return an [[IntegerValueContentV2]].
- */
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- ): ZIO[StringFormatter, Throwable, IntegerValueContentV2] =
- for {
- intValue <- ZIO
- .fromEither(jsonLDObject.getRequiredInt(IntValueAsInt))
- .mapError(BadRequestException(_))
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield IntegerValueContentV2(ApiV2Complex, intValue, comment)
-
def from(r: Resource): Either[String, IntegerValueContentV2] =
for {
intValue <- r.objectInt(IntValueAsInt)
@@ -1710,27 +1493,6 @@ case class DecimalValueContentV2(
* Constructs [[DecimalValueContentV2]] objects based on JSON-LD input.
*/
object DecimalValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[DecimalValueContentV2]].
- *
- * @param jsonLdObject the JSON-LD object.
- * @return an [[DecimalValueContentV2]].
- */
- def fromJsonLdObject(jsonLdObject: JsonLDObject): ZIO[StringFormatter, Throwable, DecimalValueContentV2] =
- ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter =>
- for {
- decimalValue <- ZIO.attempt(
- jsonLdObject.requireDatatypeValueInObject(
- key = DecimalValueAsDecimal,
- expectedDatatype = OntologyConstants.Xsd.Decimal.toSmartIri,
- validationFun = (s, errorFun) => ValuesValidator.validateBigDecimal(s).getOrElse(errorFun),
- ),
- )
- comment <- JsonLDUtil.getComment(jsonLdObject)
- } yield DecimalValueContentV2(ApiV2Complex, decimalValue, comment)
- }
-
def from(r: Resource): Either[String, DecimalValueContentV2] =
for {
decimalValue <- r.objectBigDecimal(DecimalValueAsDecimal)
@@ -1793,21 +1555,6 @@ case class BooleanValueContentV2(
* Constructs [[BooleanValueContentV2]] objects based on JSON-LD input.
*/
object BooleanValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[BooleanValueContentV2]].
- *
- * @param jsonLdObject the JSON-LD object.
- * @return an [[BooleanValueContentV2]].
- */
- def fromJsonLdObject(jsonLdObject: JsonLDObject): ZIO[StringFormatter, Throwable, BooleanValueContentV2] =
- for {
- booleanValue <- ZIO
- .fromEither(jsonLdObject.getRequiredBoolean(BooleanValueAsBoolean))
- .mapError(BadRequestException(_))
- comment <- JsonLDUtil.getComment(jsonLdObject)
- } yield BooleanValueContentV2(ApiV2Complex, booleanValue, comment)
-
def from(r: Resource): Either[String, BooleanValueContentV2] = for {
bool <- r.objectBoolean(BooleanValueAsBoolean)
comment <- objectCommentOption(r)
@@ -1874,24 +1621,6 @@ case class GeomValueContentV2(ontologySchema: OntologySchema, valueHasGeometry:
* Constructs [[GeomValueContentV2]] objects based on JSON-LD input.
*/
object GeomValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[GeomValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return an [[GeomValueContentV2]].
- */
- def fromJsonLdObject(jsonLDObject: JsonLDObject): ZIO[StringFormatter, Throwable, GeomValueContentV2] =
- for {
- geometryValueAsGeometry <- ZIO.attempt(
- jsonLDObject.requireStringWithValidation(
- GeometryValueAsGeometry,
- (s, errorFun) => ValuesValidator.validateGeometryString(s).getOrElse(errorFun),
- ),
- )
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield GeomValueContentV2(ontologySchema = ApiV2Complex, geometryValueAsGeometry, comment)
-
def from(r: Resource): Either[String, GeomValueContentV2] = for {
geomStr <- r.objectString(GeometryValueAsGeometry)
geom <- ValuesValidator.validateGeometryString(geomStr).toRight(s"Invalid geometry string: $geomStr")
@@ -1979,37 +1708,6 @@ case class IntervalValueContentV2(
* Constructs [[IntervalValueContentV2]] objects based on JSON-LD input.
*/
object IntervalValueContentV2 {
-
- /**
- * Converts a JSON-LD object to an [[IntervalValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return an [[IntervalValueContentV2]].
- */
- def fromJsonLdObject(jsonLDObject: JsonLDObject): ZIO[StringFormatter, Throwable, IntervalValueContentV2] =
- ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter =>
- for {
- intervalValueHasStart <- ZIO.attempt(
- jsonLDObject.requireDatatypeValueInObject(
- key = IntervalValueHasStart,
- expectedDatatype = OntologyConstants.Xsd.Decimal.toSmartIri,
- validationFun =
- (s, errorFun) => ValuesValidator.validateBigDecimal(s).getOrElse(errorFun),
- ),
- )
-
- intervalValueHasEnd <- ZIO.attempt(
- jsonLDObject.requireDatatypeValueInObject(
- key = IntervalValueHasEnd,
- expectedDatatype = OntologyConstants.Xsd.Decimal.toSmartIri,
- validationFun =
- (s, errorFun) => ValuesValidator.validateBigDecimal(s).getOrElse(errorFun),
- ),
- )
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield IntervalValueContentV2(ApiV2Complex, intervalValueHasStart, intervalValueHasEnd, comment)
- }
-
def from(r: Resource): Either[String, IntervalValueContentV2] = for {
intervalValueHasStart <- r.objectBigDecimal(IntervalValueHasStart)
intervalValueHasEnd <- r.objectBigDecimal(IntervalValueHasEnd)
@@ -2088,28 +1786,6 @@ case class TimeValueContentV2(
* Constructs [[TimeValueContentV2]] objects based on JSON-LD input.
*/
object TimeValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[TimeValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return an [[IntervalValueContentV2]].
- */
- def fromJsonLdObject(jsonLDObject: JsonLDObject): ZIO[StringFormatter, Throwable, TimeValueContentV2] =
- ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter =>
- for {
- valueHasTimeStamp <- ZIO.attempt(
- jsonLDObject.requireDatatypeValueInObject(
- key = TimeValueAsTimeStamp,
- expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri,
- validationFun =
- (s, errorFun) => ValuesValidator.xsdDateTimeStampToInstant(s).getOrElse(errorFun),
- ),
- )
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield TimeValueContentV2(ApiV2Complex, valueHasTimeStamp, comment)
- }
-
def from(r: Resource): Either[String, TimeValueContentV2] = for {
timeStamp <- r.objectInstant(TimeValueAsTimeStamp)
comment <- objectCommentOption(r)
@@ -2192,28 +1868,6 @@ case class HierarchicalListValueContentV2(
* Constructs [[HierarchicalListValueContentV2]] objects based on JSON-LD input.
*/
object HierarchicalListValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[HierarchicalListValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return a [[HierarchicalListValueContentV2]].
- */
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- ): ZIO[StringFormatter, Throwable, HierarchicalListValueContentV2] = ZIO.serviceWithZIO[StringFormatter] {
- implicit stringFormatter =>
- for {
- listValueAsListNode <- ZIO.attempt(
- jsonLDObject.requireIriInObject(ListValueAsListNode, stringFormatter.toSmartIriWithErr),
- )
- _ <- ZIO
- .fail(BadRequestException(s"List node IRI <$listValueAsListNode> is not a Knora data IRI"))
- .when(!listValueAsListNode.isKnoraDataIri)
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield HierarchicalListValueContentV2(ApiV2Complex, listValueAsListNode.toString, None, comment)
- }
-
def from(r: Resource, converter: IriConverter): IO[String, HierarchicalListValueContentV2] = for {
comment <- ZIO.fromEither(objectCommentOption(r))
listNode <- ZIO.fromEither(r.objectUri(ListValueAsListNode))
@@ -2283,23 +1937,6 @@ case class ColorValueContentV2(ontologySchema: OntologySchema, valueHasColor: St
* Constructs [[ColorValueContentV2]] objects based on JSON-LD input.
*/
object ColorValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[ColorValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return a [[ColorValueContentV2]].
- */
- def fromJsonLdObject(jsonLDObject: JsonLDObject): ZIO[StringFormatter, Throwable, ColorValueContentV2] =
- for {
- colorValueAsColor <- ZIO.attempt {
- val validationFun: (String, => Nothing) => String =
- (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun)
- jsonLDObject.requireStringWithValidation(ColorValueAsColor, validationFun)
- }
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield ColorValueContentV2(ApiV2Complex, colorValueAsColor, comment)
-
def from(r: Resource): Either[IRI, ColorValueContentV2] = for {
color <- r.objectString(ColorValueAsColor)
comment <- objectCommentOption(r)
@@ -2368,29 +2005,6 @@ case class UriValueContentV2(ontologySchema: OntologySchema, valueHasUri: String
* Constructs [[UriValueContentV2]] objects based on JSON-LD input.
*/
object UriValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[UriValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return a [[UriValueContentV2]].
- */
- def fromJsonLdObject(jsonLDObject: JsonLDObject): ZIO[StringFormatter, Throwable, UriValueContentV2] =
- ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter =>
- for {
- uriValueAsUri <- ZIO.attempt {
- val validationFun: (String, => Nothing) => String =
- (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun)
- jsonLDObject.requireDatatypeValueInObject(
- UriValueAsUri,
- OntologyConstants.Xsd.Uri.toSmartIri,
- validationFun,
- )
- }
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield UriValueContentV2(ApiV2Complex, uriValueAsUri, comment)
- }
-
def from(r: Resource): Either[String, UriValueContentV2] = for {
uri <- r.objectDataType(UriValueAsUri, OntologyConstants.Xsd.Uri)
comment <- objectCommentOption(r)
@@ -2463,26 +2077,6 @@ case class GeonameValueContentV2(
* Constructs [[GeonameValueContentV2]] objects based on JSON-LD input.
*/
object GeonameValueContentV2 {
-
- /**
- * Converts a JSON-LD object to a [[GeonameValueContentV2]].
- *
- * @param jsonLDObject the JSON-LD object.
- * @return a [[GeonameValueContentV2]].
- */
- def fromJsonLdObject(jsonLDObject: JsonLDObject): ZIO[StringFormatter, Throwable, GeonameValueContentV2] =
- for {
- geonameValueAsGeonameCode <- ZIO.attempt {
- val validationFun: (String, => Nothing) => String =
- (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun)
- jsonLDObject.requireStringWithValidation(
- GeonameValueAsGeonameCode,
- validationFun,
- )
- }
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield GeonameValueContentV2(ApiV2Complex, geonameValueAsGeonameCode, comment)
-
def from(r: Resource): Either[String, GeonameValueContentV2] = for {
geonameCode <- r.objectString(GeonameValueAsGeonameCode)
comment <- objectCommentOption(r)
@@ -2608,22 +2202,6 @@ case class StillImageFileValueContentV2(
* Constructs [[StillImageFileValueContentV2]] objects based on JSON-LD input.
*/
object StillImageFileValueContentV2 {
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- internalFilename: String,
- metadata: FileMetadataSipiResponse,
- ): ZIO[StringFormatter, Throwable, StillImageFileValueContentV2] =
- for {
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield StillImageFileValueContentV2(
- ontologySchema = ApiV2Complex,
- fileValue =
- FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType),
- dimX = metadata.width.getOrElse(0),
- dimY = metadata.height.getOrElse(0),
- comment = comment,
- )
-
def from(r: Resource, fileInfo: FileInfo): Either[String, StillImageFileValueContentV2] = for {
comment <- objectCommentOption(r)
meta = fileInfo.metadata
@@ -2713,30 +2291,6 @@ case class StillImageExternalFileValueContentV2(
* Constructs [[StillImageFileValueContentV2]] objects based on JSON-LD input.
*/
object StillImageExternalFileValueContentV2 {
-
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- ): ZIO[StringFormatter, Throwable, StillImageExternalFileValueContentV2] =
- for {
- comment <- JsonLDUtil.getComment(jsonLDObject)
- fromUrlType = jsonLDObject.getRequiredUri(StillImageFileValueHasExternalUrl).map(_.toString)
- // from String is kept for backwards compatibility
- fromString = jsonLDObject.getRequiredString(StillImageFileValueHasExternalUrl, FileValueHasExternalUrl)
- externalUrl <- ZIO
- .fromEither(fromUrlType.orElse(fromString).flatMap(IiifImageRequestUrl.from))
- .mapError(BadRequestException.apply)
- } yield StillImageExternalFileValueContentV2(
- ontologySchema = ApiV2Complex,
- fileValue = FileValueV2(
- "internalFilename",
- "internalMimeType",
- Some("originalFilename"),
- Some("originalMimeType"),
- ),
- externalUrl = externalUrl,
- comment = comment,
- )
-
def from(r: Resource): Either[String, StillImageExternalFileValueContentV2] = for {
externalUrlStr <- r.objectString(StillImageFileValueHasExternalUrl)
iifUrl <- IiifImageRequestUrl.from(externalUrlStr)
@@ -2878,23 +2432,6 @@ case class ArchiveFileValueContentV2(
* Constructs [[DocumentFileValueContentV2]] objects based on JSON-LD input.
*/
object DocumentFileValueContentV2 {
- def fromJsonLdObject(
- jsonLdObject: JsonLDObject,
- internalFilename: String,
- metadata: FileMetadataSipiResponse,
- ): ZIO[StringFormatter, Throwable, DocumentFileValueContentV2] =
- for {
- comment <- JsonLDUtil.getComment(jsonLdObject)
- } yield DocumentFileValueContentV2(
- ontologySchema = ApiV2Complex,
- fileValue =
- FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType),
- pageCount = metadata.numpages,
- dimX = metadata.width,
- dimY = metadata.height,
- comment,
- )
-
def from(r: Resource, info: FileInfo): Either[String, DocumentFileValueContentV2] = for {
comment <- objectCommentOption(r)
meta = info.metadata
@@ -2906,19 +2443,6 @@ object DocumentFileValueContentV2 {
* Constructs [[ArchiveFileValueContentV2]] objects based on JSON-LD input.
*/
object ArchiveFileValueContentV2 {
- def fromJsonLdObject(
- jsonLdObject: JsonLDObject,
- internalFilename: String,
- metadata: FileMetadataSipiResponse,
- ): ZIO[StringFormatter, Throwable, ArchiveFileValueContentV2] =
- for {
- comment <- JsonLDUtil.getComment(jsonLdObject)
- } yield ArchiveFileValueContentV2(
- ApiV2Complex,
- FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType),
- comment,
- )
-
def from(r: Resource, info: FileInfo): Either[String, ArchiveFileValueContentV2] = for {
comment <- objectCommentOption(r)
meta = info.metadata
@@ -2989,18 +2513,6 @@ case class TextFileValueContentV2(
* Constructs [[TextFileValueContentV2]] objects based on JSON-LD input.
*/
object TextFileValueContentV2 {
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- internalFilename: String,
- metadata: FileMetadataSipiResponse,
- ): ZIO[StringFormatter, Throwable, TextFileValueContentV2] = for {
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield TextFileValueContentV2(
- ApiV2Complex,
- FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType),
- comment,
- )
-
def from(r: Resource, info: FileInfo): Either[String, TextFileValueContentV2] = for {
comment <- objectCommentOption(r)
meta = info.metadata
@@ -3071,19 +2583,6 @@ case class AudioFileValueContentV2(
* Constructs [[AudioFileValueContentV2]] objects based on JSON-LD input.
*/
object AudioFileValueContentV2 {
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- internalFilename: String,
- metadata: FileMetadataSipiResponse,
- ): ZIO[StringFormatter, Throwable, AudioFileValueContentV2] =
- for {
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield AudioFileValueContentV2(
- ApiV2Complex,
- FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType),
- comment,
- )
-
def from(r: Resource, info: FileInfo): Either[String, AudioFileValueContentV2] = for {
comment <- objectCommentOption(r)
meta = info.metadata
@@ -3159,19 +2658,6 @@ case class MovingImageFileValueContentV2(
* Constructs [[MovingImageFileValueContentV2]] objects based on JSON-LD input.
*/
object MovingImageFileValueContentV2 {
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- internalFilename: String,
- metadata: FileMetadataSipiResponse,
- ): ZIO[StringFormatter, Throwable, MovingImageFileValueContentV2] =
- for {
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield MovingImageFileValueContentV2(
- ApiV2Complex,
- FileValueV2(internalFilename, metadata.internalMimeType, metadata.originalFilename, metadata.originalMimeType),
- comment,
- )
-
def from(r: Resource, info: FileInfo): Either[String, MovingImageFileValueContentV2] = for {
comment <- objectCommentOption(r)
meta = info.metadata
@@ -3297,30 +2783,12 @@ case class LinkValueContentV2(
* Constructs [[LinkValueContentV2]] objects based on JSON-LD input.
*/
object LinkValueContentV2 {
- def fromJsonLdObject(
- jsonLDObject: JsonLDObject,
- ): ZIO[StringFormatter, Throwable, LinkValueContentV2] = ZIO.serviceWithZIO[StringFormatter] {
- implicit stringFormatter =>
- for {
- targetIri <- ZIO.attempt(
- jsonLDObject.requireIriInObject(
- LinkValueHasTargetIri,
- stringFormatter.toSmartIriWithErr,
- ),
- )
- _ <- ZIO
- .fail(BadRequestException(s"Link target IRI <$targetIri> is not a Knora data IRI"))
- .when(!targetIri.isKnoraDataIri)
- comment <- JsonLDUtil.getComment(jsonLDObject)
- } yield LinkValueContentV2(ApiV2Complex, referredResourceIri = targetIri.toString, comment = comment)
- }
-
def from(r: Resource, converter: IriConverter): IO[String, LinkValueContentV2] =
for {
targetIri <- ZIO.fromEither(r.objectUri(LinkValueHasTargetIri))
comment <- ZIO.fromEither(objectCommentOption(r))
_ <- ZIO
- .fail(s"Link target IRI <${targetIri}> is not a Knora data IRI")
+ .fail(s"Link target IRI <$targetIri> is not a Knora data IRI")
.unlessZIO(converter.isKnoraDataIri(targetIri).mapError(_.getMessage))
} yield LinkValueContentV2(ApiV2Complex, referredResourceIri = targetIri, comment = comment)
}
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 4ff99baba4..6f4e12419a 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
@@ -34,6 +34,7 @@ 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.common.api.ApiV2.Headers.xKnoraAcceptProject
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.slice.security.Authenticator
@@ -44,8 +45,8 @@ import org.knora.webapi.store.iiif.api.SipiService
*/
final case class ResourcesRouteV2(appConfig: AppConfig)(
private implicit val runtime: Runtime[
- AppConfig & Authenticator & IriConverter & ProjectService & MessageRelay & SearchResponderV2 & SipiService &
- StringFormatter & UserService,
+ ApiComplexV2JsonLdRequestParser & AppConfig & Authenticator & IriConverter & ProjectService & MessageRelay &
+ SearchResponderV2 & SipiService & StringFormatter & UserService,
],
) extends LazyLogging {
private val sipiConfig: Sipi = appConfig.sipi
@@ -103,7 +104,11 @@ final case class ResourcesRouteV2(appConfig: AppConfig)(
requestingUser <- ZIO.serviceWithZIO[Authenticator](_.getUserADM(requestContext))
apiRequestId <- RouteUtilZ.randomUuid()
ingestState = AssetIngestState.headerAssetIngestState(requestContext.request.headers)
- requestMessage <- CreateResourceRequestV2.fromJsonLd(requestDoc, apiRequestId, requestingUser, ingestState)
+ requestMessage <- ZIO
+ .serviceWithZIO[ApiComplexV2JsonLdRequestParser](
+ _.createResourceRequestV2(jsonRequest, ingestState, requestingUser, apiRequestId),
+ )
+ .mapError(BadRequestException(_))
// check for each value which represents a file value if the file's MIME type is allowed
_ <- checkMimeTypesForFileValueContents(requestMessage.createResource.flatValues)
} yield requestMessage
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 1656b9d96e..a9b08dcc7e 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
@@ -12,23 +12,35 @@ import zio.ZLayer
import java.time.Instant
import java.util.UUID
+import scala.collection.immutable.Seq
import scala.jdk.CollectionConverters.*
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.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.valuemessages.*
import org.knora.webapi.messages.v2.responder.valuemessages.ValueContentV2.FileInfo
import org.knora.webapi.routing.v2.AssetIngestState
+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.KnoraProject.Shortcode
+import org.knora.webapi.slice.admin.domain.model.User
+import org.knora.webapi.slice.admin.domain.model.UserIri
+import org.knora.webapi.slice.admin.domain.service.ProjectService
+import org.knora.webapi.slice.admin.domain.service.UserService
import org.knora.webapi.slice.common.KnoraIris.*
import org.knora.webapi.slice.common.KnoraIris.ResourceClassIri as KResourceClassIri
-import org.knora.webapi.slice.common.KnoraIris.ResourceIri
import org.knora.webapi.slice.common.KnoraIris.ResourceIri as KResourceIri
import org.knora.webapi.slice.common.jena.JenaConversions.given
import org.knora.webapi.slice.common.jena.ModelOps
@@ -42,8 +54,142 @@ final case class ApiComplexV2JsonLdRequestParser(
converter: IriConverter,
messageRelay: MessageRelay,
sipiService: SipiService,
+ projectService: ProjectService,
+ userService: UserService,
) {
+ 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,
+ )
+ }
+
+ private def extractValues(
+ r: Resource,
+ shortcode: Shortcode,
+ ingestState: AssetIngestState,
+ ): IO[String, Map[SmartIri, Seq[CreateValueInNewResourceV2]]] =
+ val filteredProperties = Seq(
+ RDF.`type`.toString,
+ Rdfs.Label,
+ KnoraApiV2Complex.AttachedToProject,
+ KnoraApiV2Complex.AttachedToUser,
+ KnoraApiV2Complex.HasPermissions,
+ KnoraApiV2Complex.CreationDate,
+ )
+ val valueStatements = r
+ .listProperties()
+ .asScala
+ .filter(p => !filteredProperties.contains(p.getPredicate.toString))
+ .toSeq
+ ZIO
+ .foreach(valueStatements)(valueStatementAsContent(_, shortcode, ingestState))
+ .map(_.groupMap(_._1.smartIri)(_._2))
+
+ private def valueStatementAsContent(
+ statement: Statement,
+ shortcode: Shortcode,
+ ingestState: AssetIngestState,
+ ): IO[String, (PropertyIri, CreateValueInNewResourceV2)] =
+ val valueResource = statement.getObject.asResource()
+ for {
+ typ <- ZIO.fromEither(valueResource.rdfsType.toRight("No rdf:type found for value."))
+ filename <- ZIO.fromEither(valueFileValueFilename(valueResource))
+ cnt <- getValueContent(typ, valueResource, filename, shortcode, ingestState)
+ propertyIri <- valuePropertyIri(statement)
+ customValueIri <- valueIri(valueResource)
+ customValueUuid <- ZIO.fromEither(valueHasUuid(valueResource))
+ customValueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource))
+ permissions <- ZIO.fromEither(valuePermissions(valueResource))
+ } yield (
+ propertyIri,
+ CreateValueInNewResourceV2(
+ cnt,
+ customValueIri.map(_.smartIri),
+ customValueUuid,
+ customValueCreationDate,
+ permissions,
+ ),
+ )
+
+ 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))
+ userIri <- ZIO.foreach(userStr)(iri => ZIO.fromEither(UserIri.from(iri)))
+ user <- ZIO.foreach(userIri)(iri => checkUser(requestingUser, iri, projectIri))
+ } yield user.getOrElse(requestingUser)
+
+ private def checkUser(requestingUser: User, userIri: UserIri, projectIri: ProjectIri): IO[String, User] =
+ requestingUser match {
+ case _ if requestingUser.id == userIri.value => ZIO.succeed(requestingUser)
+ case _
+ 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.",
+ )
+ case _ => userService.findUserByIri(userIri).orDie.someOrFail(s"User '${userIri.value}' not found")
+ }
+
+ def attachedToProject(r: Resource): IO[String, Project] =
+ for {
+ projectIri <- ZIO.fromEither(r.objectUri(AttachedToProject)).flatMap(iri => ZIO.fromEither(ProjectIri.from(iri)))
+ project <- projectService.findById(projectIri).orDie.someOrFail(s"Project ${projectIri.value} not found")
+ } yield project
+
+ 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 updateValueV2fromJsonLd(str: String, ingestState: AssetIngestState): IO[String, UpdateValueV2] =
ZIO.scoped {
for {
@@ -53,8 +199,8 @@ final case class ApiComplexV2JsonLdRequestParser(
resourceClassIri <- resourceClassIri(resource)
valueStatement <- valueStatement(resource)
valuePropertyIri <- valuePropertyIri(valueStatement)
- valueType <- valueType(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))
@@ -99,22 +245,6 @@ final case class ApiComplexV2JsonLdRequestParser(
} 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 {
@@ -124,13 +254,13 @@ final case class ApiComplexV2JsonLdRequestParser(
resourceClassIri <- resourceClassIri(resource)
valueStatement <- valueStatement(resource)
valuePropertyIri <- valuePropertyIri(valueStatement)
- valueType <- valueType(valueStatement)
- valueResource = valueStatement.getObject.asResource()
+ valueResource <- ZIO.fromEither(valueStatement.objectAsResource())
valueIri <- valueIri(valueResource)
valueUuid <- ZIO.fromEither(valueHasUuid(valueResource))
valueCreationDate <- ZIO.fromEither(valueCreationDate(valueResource))
valuePermissions <- ZIO.fromEither(valuePermissions(valueResource))
valueFileValueFilename <- ZIO.fromEither(valueFileValueFilename(valueResource))
+ valueType <- valueType(valueResource)
valueContent <-
getValueContent(valueType.toString, valueResource, valueFileValueFilename, resourceIri.shortcode, ingestState)
} yield CreateValueV2(
@@ -147,11 +277,17 @@ 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) =>
- converter
- .asSmartIri(r.uri.getOrElse(""))
- .mapError(_.getMessage)
- .flatMap(iri => ZIO.fromEither(KResourceIri.from(iri)))
+ ZIO
+ .foreach(r.uri)(
+ converter.asSmartIri(_).mapError(_.getMessage).flatMap(iri => ZIO.fromEither(KResourceIri.from(iri))),
+ )
.map((r, _))
}
@@ -167,8 +303,8 @@ final case class ApiComplexV2JsonLdRequestParser(
.mapError(_.getMessage)
.flatMap(iri => ZIO.fromEither(PropertyIri.from(iri)))
- private def valueType(stmt: Statement) = ZIO
- .fromEither(stmt.objectAsResource().flatMap(_.rdfsType.toRight("No rdf:type found for value.")))
+ private def valueType(resource: Resource) = ZIO
+ .fromEither(resource.rdfsType.toRight("No rdf:type found for value."))
.orElseFail(s"No value type found for value.")
.flatMap(converter.asSmartIri(_).mapError(_.getMessage))
diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala
index d494165d3d..ccc2b86aef 100644
--- a/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala
+++ b/webapi/src/main/scala/org/knora/webapi/slice/common/jena/ModelOps.scala
@@ -31,10 +31,10 @@ object ModelOps { self =>
statementOption(s, p).toRight(s"Statement not found '${s.getURI} ${p.getURI} ?o .'")
def singleRootResource: Either[String, Resource] =
- val objSeen = model.listObjects().asScala.collect { case r: Resource => Option(r.getURI) }.toSet.flatten
- val subSeen = model.listSubjects().asScala.collect { case r: Resource => Option(r.getURI) }.toSet.flatten
- (subSeen -- objSeen) match {
- case iris if iris.size == 1 => model.resource(iris.head)
+ val subs = model.listSubjects().asScala.toSet
+ val objs = model.listObjects().asScala.collect { case r: Resource => r }.toSet
+ (subs -- objs) match {
+ case iris if iris.size == 1 => Right(iris.head)
case iris if iris.isEmpty => Left("No root resource found in model")
case iris => Left(s"Multiple root resources found in model: ${iris.mkString(", ")}")
}
diff --git a/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala
index 918bc9b8e0..af0f7d98ed 100644
--- a/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala
+++ b/webapi/src/test/scala/org/knora/webapi/slice/common/ApiComplexV2JsonLdRequestParserSpec.scala
@@ -14,6 +14,7 @@ import zio.test.*
import java.time.Instant
import org.knora.webapi.ApiV2Complex
+import org.knora.webapi.config.AppConfig
import org.knora.webapi.core.MessageRelayLive
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.util.CalendarNameGregorian
@@ -41,9 +42,15 @@ import org.knora.webapi.messages.v2.responder.valuemessages.TextValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.TextValueType.UnformattedText
import org.knora.webapi.messages.v2.responder.valuemessages.TimeValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.UriValueContentV2
+import org.knora.webapi.responders.IriService
import org.knora.webapi.routing.v2.AssetIngestState.AssetIngested
+import org.knora.webapi.slice.admin.domain.model.*
+import org.knora.webapi.slice.admin.domain.repo.*
+import org.knora.webapi.slice.admin.domain.service.*
+import org.knora.webapi.slice.admin.repo.service.*
import org.knora.webapi.slice.common.JsonLdTestUtil.JsonLdTransformations
import org.knora.webapi.slice.common.KnoraIris.*
+import org.knora.webapi.slice.ontology.repo.service.OntologyRepoInMemory
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.slice.resources.IiifImageRequestUrl
import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse
@@ -51,6 +58,7 @@ import org.knora.webapi.store.iiif.api.SipiService
import org.knora.webapi.store.iiif.impl.SipiServiceMock
import org.knora.webapi.store.iiif.impl.SipiServiceMock.SipiMockMethodName.GetFileMetadataFromDspIngest
import org.knora.webapi.store.iiif.impl.SipiServiceMock.SipiMockMethodName.GetFileMetadataFromSipiTemp
+import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory
object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault {
private val sf = StringFormatter.getInitializedTestInstance
@@ -745,10 +753,27 @@ object ApiComplexV2JsonLdRequestParserSpec extends ZIOSpecDefault {
}
},
).provideSome[Scope](
+ AdministrativePermissionRepoInMemory.layer,
+ AdministrativePermissionService.layer,
+ ApiComplexV2JsonLdRequestParser.layer,
+ GroupService.layer,
IriConverter.layer,
+ IriService.layer,
+ KnoraGroupRepoInMemory.layer,
+ KnoraGroupService.layer,
+ KnoraProjectRepoInMemory.layer,
+ KnoraProjectService.layer,
+ KnoraUserRepoInMemory.layer,
+ KnoraUserService.layer,
+ KnoraUserToUserConverter.layer,
MessageRelayLive.layer,
- StringFormatter.test,
- ApiComplexV2JsonLdRequestParser.layer,
+ OntologyRepoInMemory.emptyLayer,
+ PasswordService.layer,
+ ProjectService.layer,
SipiServiceMock.layer,
+ StringFormatter.test,
+ TriplestoreServiceInMemory.emptyLayer,
+ UserService.layer,
+ AppConfig.layer,
)
}
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 cbe90d1fca..f21cec191b 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
@@ -8,6 +8,8 @@ package org.knora.webapi.slice.common.jena
import zio.*
import zio.test.*
+import org.knora.webapi.slice.common.jena.ModelOps.singleRootResource
+
object ModelOpsSpec extends ZIOSpecDefault {
private val jsonLd = """{
@@ -40,7 +42,75 @@ object ModelOpsSpec extends ZIOSpecDefault {
}
}
""".stripMargin
+
+ private val singleRootResourceSuite = suite("singleRootResource")(
+ test("should return the single root resource") {
+ for {
+ model <- ModelOps
+ .fromTurtle("""
+ |@prefix ex: .
+ |ex:root
+ | ex:hasChild ex:child ;
+ | ex:hasBlankNode [
+ | ex:hasOther ex:foo
+ | ] ;
+ | ex:hasOther ex:bar .
+ |""".stripMargin)
+
+ } yield assertTrue(model.singleRootResource.map(_.getURI) == Right("http://example.org/root"))
+ },
+ test("should fail with no resources given") {
+ for {
+ model <- ModelOps.fromTurtle("")
+ } yield assertTrue(
+ model.singleRootResource == Left("No root resource found in model"),
+ )
+ },
+ test("should fail with more than a single root resource, uri resources") {
+ for {
+ model <- ModelOps
+ .fromTurtle("""
+ |@prefix ex: .
+ |ex:root1 ex:hasChild ex:child1 .
+ |ex:root2 ex:hasChild ex:child2 .
+ |""".stripMargin)
+ } yield assertTrue(
+ model.singleRootResource == Left(
+ "Multiple root resources found in model: http://example.org/root1, http://example.org/root2",
+ ),
+ )
+ },
+ test("should fail with more than a single root resource, blank node") {
+ for {
+ model <- ModelOps
+ .fromTurtle("""
+ |@prefix ex: .
+ |[
+ | ex:hasChild ex:child1
+ |] .
+ |ex:root2 ex:hasChild ex:child2 .
+ |""".stripMargin)
+ } yield assertTrue(
+ model.singleRootResource.left.map(_.startsWith("Multiple root resources found in model:")) == Left(true),
+ )
+ },
+ test("should return blank node if that is the single root resource") {
+ for {
+ model <- ModelOps
+ .fromTurtle("""
+ |@prefix ex: .
+ |[
+ | ex:hasChild ex:child1
+ |].
+ |""".stripMargin)
+ } yield assertTrue(
+ model.singleRootResource.map(_.isAnon) == Right(true),
+ )
+ },
+ )
+
val spec = suite("ModelOps")(
+ singleRootResourceSuite,
suite("fromJsonLd")(
test("should parse the json ld") {
ModelOps.fromJsonLd(jsonLd).flatMap { model =>