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 =>