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 a513528d6b..8c63845435 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 @@ -21,12 +21,10 @@ 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 import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex.* -import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.ValuesValidator @@ -53,11 +51,6 @@ import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.util.WithAsIs -/** - * A tagging trait for requests handled by [[org.knora.webapi.responders.v2.ValuesResponderV2]]. - */ -sealed trait ValuesResponderRequestV2 extends KnoraRequestV2 with RelayedMessage - /** * Represents a successful response to a create value Request. * @@ -228,38 +221,6 @@ object DeleteValueV2 { } } -/** - * Requests SPARQL for creating multiple values in a new, empty resource. The resource ''must'' be a new, empty - * resource, i.e. it must have no values. This message is used only internally by Knora, and is not part of the Knora - * v1 API. All pre-update checks must already have been performed before this message is sent. Specifically, the - * sender must ensure that: - * - * - The requesting user has permission to add values to the resource. - * - Each submitted value is consistent with the `knora-base:objectClassConstraint` of the property that is supposed - * to point to it. - * - The resource class has a suitable cardinality for each submitted value. - * - All required values are provided. - * - Redundant values are not submitted. - * - Any custom permissions in values have been validated and correctly formatted. - * - The target resources of link values and standoff links exist, if they are expected to exist. - * - The list nodes referred to by list values exist. - * - * A successful response will be a [[GenerateSparqlToCreateMultipleValuesResponseV2]]. - * - * @param resourceIri the IRI of the resource in which values are to be created. - * @param values a map of property IRIs to the values to be added for each property. - * @param creationDate an xsd:dateTimeStamp that will be attached to the values. - * @param requestingUser the user that is creating the values. - */ -case class GenerateSparqlToCreateMultipleValuesRequestV2( - resourceIri: IRI, - values: Map[SmartIri, Seq[GenerateSparqlForValueInNewResourceV2]], - creationDate: Instant, - requestingUser: User, -) extends ValuesResponderRequestV2 { - lazy val flatValues: Iterable[GenerateSparqlForValueInNewResourceV2] = values.values.flatten -} - case class GenerateSparqlForValueInNewResourceV2( valueContent: ValueContentV2, customValueIri: Option[SmartIri], @@ -268,22 +229,6 @@ case class GenerateSparqlForValueInNewResourceV2( permissions: String, ) -/** - * Represents a response to a [[GenerateSparqlToCreateMultipleValuesRequestV2]], providing a string that can be - * included in the `INSERT DATA` clause of a SPARQL update operation to create the requested values. - * - * @param insertSparql a string containing statements that must be inserted into the INSERT clause of the SPARQL - * update that will create the values. - * @param unverifiedValues a map of property IRIs to [[UnverifiedValueV2]] objects describing - * the values that should have been created. - * @param hasStandoffLink `true` if the property `knora-base:hasStandoffLinkToValue` was automatically added. - */ -case class GenerateSparqlToCreateMultipleValuesResponseV2( - insertSparql: String, - unverifiedValues: Map[SmartIri, Seq[UnverifiedValueV2]], - hasStandoffLink: Boolean, -) - /** * Provides information about the deletion of a resource or value. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index a42cf85771..ef8c2dd3b8 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -15,7 +15,6 @@ import dsp.valueobjects.UuidUtil import org.knora.webapi.* import org.knora.webapi.SchemaRendering.apiV2SchemaWithOption import org.knora.webapi.config.AppConfig -import org.knora.webapi.core.MessageHandler import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.* import org.knora.webapi.messages.IriConversions.* @@ -35,7 +34,6 @@ import org.knora.webapi.messages.v2.responder.resourcemessages.* import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService -import org.knora.webapi.responders.Responder import org.knora.webapi.responders.admin.PermissionsResponder import org.knora.webapi.slice.admin.domain.model.Permission import org.knora.webapi.slice.admin.domain.model.User @@ -48,7 +46,6 @@ import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update -import org.knora.webapi.util.ZioHelper /** * Handles requests to read and write Knora values. @@ -83,19 +80,7 @@ final case class ValuesResponderV2Live( triplestoreService: TriplestoreService, permissionsResponder: PermissionsResponder, )(implicit val stringFormatter: StringFormatter) - extends ValuesResponderV2 - with MessageHandler { - - override def isResponsibleFor(message: ResponderRequest): Boolean = message.isInstanceOf[ValuesResponderRequestV2] - - /** - * Receives a message of type [[ValuesResponderRequestV2]], and returns an appropriate response message. - */ - override def handle(msg: ResponderRequest): Task[Any] = msg match { - case createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2 => - generateSparqlToCreateMultipleValuesV2(createMultipleValuesRequest) - case other => Responder.handleUnexpectedMessage(other, this.getClass.getName) - } + extends ValuesResponderV2 { /** * Creates a new value in an existing resource. @@ -468,7 +453,7 @@ final case class ValuesResponderV2Live( for { // Make a new value UUID. - newValueUUID <- makeNewValueUUID(maybeValueIri, maybeValueUUID) + newValueUUID <- ValuesResponderV2Live.makeNewValueUUID(maybeValueIri, maybeValueUUID) // Make an IRI for the new value. newValueIri <- @@ -555,7 +540,7 @@ final case class ValuesResponderV2Live( // Make a new value UUID. for { - newValueUUID <- makeNewValueUUID(maybeValueIri, maybeValueUUID) + newValueUUID <- ValuesResponderV2Live.makeNewValueUUID(maybeValueIri, maybeValueUUID) sparqlTemplateLinkUpdate <- incrementLinkValue( sourceResourceInfo = resourceInfo, @@ -592,248 +577,6 @@ final case class ValuesResponderV2Live( creationDate = creationDate, ) - /** - * Represents SPARQL generated to create one of multiple values in a new resource. - * - * @param insertSparql the generated SPARQL. - * @param unverifiedValue an [[UnverifiedValueV2]] representing the value that is to be created. - */ - private case class InsertSparqlWithUnverifiedValue(insertSparql: String, unverifiedValue: UnverifiedValueV2) - - /** - * Generates SPARQL for creating multiple values. - * - * @param createMultipleValuesRequest the request to create multiple values. - * @return a [[GenerateSparqlToCreateMultipleValuesResponseV2]] containing the generated SPARQL and information - * about the values to be created. - */ - private def generateSparqlToCreateMultipleValuesV2( - createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2, - ): Task[GenerateSparqlToCreateMultipleValuesResponseV2] = - for { - // Generate SPARQL to create links and LinkValues for standoff links in text values. - sparqlForStandoffLinks <- - generateInsertSparqlForStandoffLinksInMultipleValues( - createMultipleValuesRequest, - ) - - // Generate SPARQL for each value. - sparqlForPropertyValueFutures = - createMultipleValuesRequest.values.map { - case (propertyIri: SmartIri, valuesToCreate: Seq[GenerateSparqlForValueInNewResourceV2]) => - val values = valuesToCreate.zipWithIndex.map { - case (valueToCreate: GenerateSparqlForValueInNewResourceV2, valueHasOrder: Int) => - generateInsertSparqlWithUnverifiedValue( - resourceIri = createMultipleValuesRequest.resourceIri, - propertyIri = propertyIri, - valueToCreate = valueToCreate, - valueHasOrder = valueHasOrder, - resourceCreationDate = createMultipleValuesRequest.creationDate, - requestingUser = createMultipleValuesRequest.requestingUser, - ) - } - propertyIri -> ZIO.collectAll(values) - } - - sparqlForPropertyValues <- ZioHelper.sequence(sparqlForPropertyValueFutures) - - // Concatenate all the generated SPARQL. - allInsertSparql: String = - sparqlForPropertyValues.values.flatten - .map(_.insertSparql) - .mkString("\n\n") + "\n\n" + sparqlForStandoffLinks.getOrElse("") - - // Collect all the unverified values. - unverifiedValues: Map[SmartIri, Seq[UnverifiedValueV2]] = - sparqlForPropertyValues.map { case (propertyIri, unverifiedValuesWithSparql) => - propertyIri -> unverifiedValuesWithSparql.map( - _.unverifiedValue, - ) - } - } yield GenerateSparqlToCreateMultipleValuesResponseV2( - insertSparql = allInsertSparql, - unverifiedValues = unverifiedValues, - hasStandoffLink = sparqlForStandoffLinks.isDefined, - ) - - /** - * Generates SPARQL to create one of multiple values in a new resource. - * - * @param resourceIri the IRI of the resource. - * @param propertyIri the IRI of the property that will point to the value. - * @param valueToCreate the value to be created. - * @param valueHasOrder the value's `knora-base:valueHasOrder`. - * @param resourceCreationDate the creation date of the resource. - * @param requestingUser the user making the request. - * @return a [[InsertSparqlWithUnverifiedValue]] containing the generated SPARQL and an [[UnverifiedValueV2]]. - */ - private def generateInsertSparqlWithUnverifiedValue( - resourceIri: IRI, - propertyIri: SmartIri, - valueToCreate: GenerateSparqlForValueInNewResourceV2, - valueHasOrder: Int, - resourceCreationDate: Instant, - requestingUser: User, - ): Task[InsertSparqlWithUnverifiedValue] = - for { - // Make new value UUID. - newValueUUID <- makeNewValueUUID(valueToCreate.customValueIri, valueToCreate.customValueUUID) - newValueIri <- - iriService.checkOrCreateEntityIri( - valueToCreate.customValueIri, - stringFormatter.makeRandomValueIri(resourceIri, Some(newValueUUID)), - ) - - // Make a creation date for the value. If a custom creation date is given for a value, consider that otherwise - // use resource creation date for the value. - valueCreationDate: Instant = valueToCreate.customValueCreationDate.getOrElse(resourceCreationDate) - - // Generate the SPARQL. - insertSparql: String = - valueToCreate.valueContent match { - case linkValueContentV2: LinkValueContentV2 => - // We're creating a link. - - // Construct a SparqlTemplateLinkUpdate to tell the SPARQL template how to create - // the link and its LinkValue. - val sparqlTemplateLinkUpdate = SparqlTemplateLinkUpdate( - linkPropertyIri = propertyIri.fromLinkValuePropToLinkProp, - directLinkExists = false, - insertDirectLink = true, - deleteDirectLink = false, - linkValueExists = false, - linkTargetExists = linkValueContentV2.referredResourceExists, - newLinkValueIri = newValueIri, - linkTargetIri = linkValueContentV2.referredResourceIri, - currentReferenceCount = 0, - newReferenceCount = 1, - newLinkValueCreator = requestingUser.id, - newLinkValuePermissions = valueToCreate.permissions, - ) - - // Generate SPARQL for the link. - sparql.v2.txt - .generateInsertStatementsForCreateLink( - resourceIri = resourceIri, - linkUpdate = sparqlTemplateLinkUpdate, - creationDate = valueCreationDate, - newValueUUID = newValueUUID, - maybeComment = valueToCreate.valueContent.comment, - maybeValueHasOrder = Some(valueHasOrder), - ) - .toString() - - case otherValueContentV2 => - // We're creating an ordinary value. Generate SPARQL for it. - sparql.v2.txt - .generateInsertStatementsForCreateValue( - resourceIri = resourceIri, - propertyIri = propertyIri, - value = otherValueContentV2, - newValueIri = newValueIri, - newValueUUID = newValueUUID, - linkUpdates = Seq.empty[ - SparqlTemplateLinkUpdate, - ], // This is empty because we have to generate SPARQL for standoff links separately. - valueCreator = requestingUser.id, - valuePermissions = valueToCreate.permissions, - creationDate = valueCreationDate, - maybeValueHasOrder = Some(valueHasOrder), - ) - .toString() - } - } yield InsertSparqlWithUnverifiedValue( - insertSparql = insertSparql, - unverifiedValue = UnverifiedValueV2( - newValueIri = newValueIri, - newValueUUID = newValueUUID, - valueContent = valueToCreate.valueContent.unescape, - permissions = valueToCreate.permissions, - creationDate = valueCreationDate, - ), - ) - - /** - * When processing a request to create multiple values, generates SPARQL for standoff links in text values. - * - * @param createMultipleValuesRequest the request to create multiple values. - * @return SPARQL INSERT statements. - */ - private def generateInsertSparqlForStandoffLinksInMultipleValues( - createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2, - ): Task[Option[String]] = { - // To create LinkValues for the standoff links in the values to be created, we need to compute - // the initial reference count of each LinkValue. This is equal to the number of TextValues in the resource - // that have standoff links to a particular target resource. - - // First, get the standoff link targets from all the text values to be created. - val standoffLinkTargetsPerTextValue: Vector[Set[IRI]] = - createMultipleValuesRequest.flatValues.foldLeft(Vector.empty[Set[IRI]]) { - case (standoffLinkTargetsAcc: Vector[Set[IRI]], createValueV2: GenerateSparqlForValueInNewResourceV2) => - createValueV2.valueContent match { - case textValueContentV2: TextValueContentV2 - if textValueContentV2.standoffLinkTagTargetResourceIris.nonEmpty => - standoffLinkTargetsAcc :+ textValueContentV2.standoffLinkTagTargetResourceIris - - case _ => standoffLinkTargetsAcc - } - } - - if (standoffLinkTargetsPerTextValue.nonEmpty) { - // Combine those resource references into a single list, so if there are n text values with a link to - // some IRI, the list will contain that IRI n times. - val allStandoffLinkTargets: Vector[IRI] = standoffLinkTargetsPerTextValue.flatten - - // Now we need to count the number of times each IRI occurs in allStandoffLinkTargets. To do this, first - // use groupBy(identity). The groupBy method takes a function that returns a key for each item in the - // collection, and makes a Map in which items with the same key are grouped together. The identity - // function just returns its argument. So groupBy(identity) makes a Map[IRI, Vector[IRI]] in which each - // IRI points to a sequence of the same IRI repeated as many times as it occurred in allStandoffLinkTargets. - val allStandoffLinkTargetsGrouped: Map[IRI, Vector[IRI]] = allStandoffLinkTargets.groupBy(identity) - - // Replace each Vector[IRI] with its size. That's the number of text values containing - // standoff links to that IRI. - val initialReferenceCounts: Map[IRI, Int] = allStandoffLinkTargetsGrouped.view.mapValues(_.size).toMap - - // For each standoff link target IRI, construct a SparqlTemplateLinkUpdate to create a hasStandoffLinkTo property - // and one LinkValue with its initial reference count. - val standoffLinkUpdatesFutures: Seq[Task[SparqlTemplateLinkUpdate]] = initialReferenceCounts.toSeq.map { - case (targetIri, initialReferenceCount) => - for { - newValueIri <- makeUnusedValueIri(createMultipleValuesRequest.resourceIri) - } yield SparqlTemplateLinkUpdate( - linkPropertyIri = OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, - directLinkExists = false, - insertDirectLink = true, - deleteDirectLink = false, - linkValueExists = false, - linkTargetExists = - true, // doesn't matter, the generateInsertStatementsForStandoffLinks template doesn't use it - newLinkValueIri = newValueIri, - linkTargetIri = targetIri, - currentReferenceCount = 0, - newReferenceCount = initialReferenceCount, - newLinkValueCreator = KnoraUserRepo.builtIn.SystemUser.id.value, - newLinkValuePermissions = standoffLinkValuePermissions, - ) - } - for { - standoffLinkUpdates <- ZIO.collectAll(standoffLinkUpdatesFutures) - // Generate SPARQL INSERT statements based on those SparqlTemplateLinkUpdates. - sparqlInsert = - sparql.v2.txt - .generateInsertStatementsForStandoffLinks( - resourceIri = createMultipleValuesRequest.resourceIri, - linkUpdates = standoffLinkUpdates, - creationDate = createMultipleValuesRequest.creationDate, - ) - .toString() - } yield Some(sparqlInsert) - } else { - ZIO.succeed(None) - } - } - /** * Creates a new version of an existing value. * @@ -2394,6 +2137,10 @@ final case class ValuesResponderV2Live( */ private def makeUnusedValueIri(resourceIri: IRI): Task[IRI] = iriService.makeUnusedIri(stringFormatter.makeRandomValueIri(resourceIri)) +} + +object ValuesResponderV2Live { + val layer = ZLayer.derive[ValuesResponderV2Live] /** * Make a new value UUID considering optional custom value UUID and custom value IRI. @@ -2405,50 +2152,25 @@ final case class ValuesResponderV2Live( * @param maybeCustomUUID the optional value UUID. * @return the new value UUID. */ - private def makeNewValueUUID( + def makeNewValueUUID( maybeCustomIri: Option[SmartIri], maybeCustomUUID: Option[UUID], ): IO[BadRequestException, UUID] = - // Is there any custom value UUID given? - maybeCustomUUID match { - case Some(customValueUUID) => - // Yes. Check that if a custom IRI is given, it ends with the same UUID - if (maybeCustomIri.flatMap(_.getUuid).forall(_ == customValueUUID)) { - ZIO.succeed(customValueUUID) - } else { - ZIO.fail( - BadRequestException( - s" Given custom IRI ${maybeCustomIri.get} should contain the given custom UUID ${UuidUtil - .base64Encode(customValueUUID)}.", - ), - ) - } - case None => - // No. Is there a custom IRI given? - maybeCustomIri match { - case Some(customIri: SmartIri) => - // Yes. Get the UUID from the given value IRI - ZIO - .fromOption(customIri.getUuid) - .orElseFail(BadRequestException(s"Invalid UUID in IRI: $customIri")) - case None => Random.nextUUID - } + (maybeCustomIri, maybeCustomUUID) match { + case (Some(customIri: SmartIri), Some(customValueUUID)) => combineCustoms(customIri, customValueUUID) + case (None, Some(customValueUUID)) => ZIO.succeed(customValueUUID) + case (Some(customIri), None) => + ZIO.fromOption(customIri.getUuid).orElseFail(BadRequestException(s"Invalid UUID in IRI: $customIri")) + case (None, None) => Random.nextUUID + } -} -object ValuesResponderV2Live { - val layer = ZLayer.fromZIO { - for { - config <- ZIO.service[AppConfig] - is <- ZIO.service[IriService] - mr <- ZIO.service[MessageRelay] - pu <- ZIO.service[PermissionUtilADM] - ru <- ZIO.service[ResourceUtilV2] - ts <- ZIO.service[TriplestoreService] - sr <- ZIO.service[SearchResponderV2] - sf <- ZIO.service[StringFormatter] - pr <- ZIO.service[PermissionsResponder] - handler <- mr.subscribe(ValuesResponderV2Live(config, is, mr, pu, ru, sr, ts, pr)(sf)) - } yield handler - } + private def combineCustoms(iri: SmartIri, uuid: UUID): IO[BadRequestException, UUID] = + if (iri.getUuid.contains(uuid)) ZIO.succeed(uuid) + else + ZIO.fail( + BadRequestException( + s"Given custom IRI $iri should contain the given custom UUID ${UuidUtil.base64Encode(uuid)}.", + ), + ) } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala index 6776692d53..50a3ae3a86 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/resources/CreateResourceV2Handler.scala @@ -11,7 +11,6 @@ import zio.* import java.time.Instant import dsp.errors.* -import dsp.valueobjects.Iri import org.knora.webapi.* import org.knora.webapi.config.AppConfig import org.knora.webapi.core.MessageRelay @@ -19,7 +18,10 @@ import org.knora.webapi.messages.* import org.knora.webapi.messages.IriConversions.* import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionsStringForResourceClassGetADM import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionsStringResponseADM +import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionADM +import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionType import org.knora.webapi.messages.admin.responder.permissionsmessages.ResourceCreateOperation +import org.knora.webapi.messages.twirl.SparqlTemplateLinkUpdate import org.knora.webapi.messages.twirl.SparqlTemplateResourceToCreate import org.knora.webapi.messages.twirl.queries.sparql import org.knora.webapi.messages.util.* @@ -35,8 +37,11 @@ import org.knora.webapi.responders.IriService import org.knora.webapi.responders.admin.PermissionsResponder import org.knora.webapi.responders.v2.* import org.knora.webapi.slice.admin.api.model.* +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.KnoraGroupRepo import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo +import org.knora.webapi.slice.admin.domain.service.KnoraUserRepo import org.knora.webapi.slice.admin.domain.service.ProjectService import org.knora.webapi.slice.ontology.domain.model.Cardinality.AtLeastOne import org.knora.webapi.slice.ontology.domain.model.Cardinality.ExactlyOne @@ -267,24 +272,29 @@ final case class CreateResourceV2Handler( ProjectService.projectDataNamedGraphV2(createResourceRequestV2.createResource.projectADM).value // Generate SPARQL for creating the resource. - sparqlUpdate = sparql.v2.txt.createNewResources( + sparqlUpdate = sparql.v2.txt.createNewResource( dataNamedGraph = dataNamedGraph, - resourcesToCreate = Seq(resourceReadyToCreate.sparqlTemplateResourceToCreate), + resourceToCreate = resourceReadyToCreate.sparqlTemplateResourceToCreate, projectIri = createResourceRequestV2.createResource.projectADM.id, creatorIri = createResourceRequestV2.requestingUser.id, ) // Do the update. _ <- triplestore.query(Update(sparqlUpdate)) - // Verify that the resource was created correctly. + // Verify that the resource was created. previewOfCreatedResource <- verifyResource( resourceReadyToCreate = resourceReadyToCreate, - projectIri = createResourceRequestV2.createResource.projectADM.id, requestingUser = createResourceRequestV2.requestingUser, ) } yield previewOfCreatedResource } + private case class GenerateSparqlToCreateMultipleValuesResponseV2( + insertSparql: String, + unverifiedValues: Map[SmartIri, Seq[UnverifiedValueV2]], + hasStandoffLink: Boolean, + ) + /** * Generates a [[SparqlTemplateResourceToCreate]] describing SPARQL for creating a resource and its values. * This method does pre-update checks that have to be done for each new resource individually, even when @@ -438,16 +448,12 @@ final case class CreateResourceV2Handler( ) // Ask the values responder for SPARQL for generating the values. - sparqlForValuesResponse <- - messageRelay - .ask[GenerateSparqlToCreateMultipleValuesResponseV2]( - GenerateSparqlToCreateMultipleValuesRequestV2( - resourceIri = resourceIri, - values = valuesWithValidatedPermissions, - creationDate = creationDate, - requestingUser = requestingUser, - ), - ) + sparqlForValuesResponse <- generateSparqlToCreateMultipleValuesV2( + resourceIri = resourceIri, + values = valuesWithValidatedPermissions, + creationDate = creationDate, + requestingUser = requestingUser, + ) } yield ResourceReadyToCreate( sparqlTemplateResourceToCreate = SparqlTemplateResourceToCreate( resourceIri = resourceIri, @@ -757,101 +763,287 @@ final case class CreateResourceV2Handler( } /** - * Checks that a resource was created correctly. + * Checks that a resource was created. * * @param resourceReadyToCreate the resource that should have been created. * @param projectIri the IRI of the project in which the resource should have been created. - * * @param requestingUser the user that attempted to create the resource. * @return a preview of the resource that was created. */ private def verifyResource( resourceReadyToCreate: ResourceReadyToCreate, - projectIri: IRI, requestingUser: User, ): Task[ReadResourcesSequenceV2] = { val resourceIri = resourceReadyToCreate.sparqlTemplateResourceToCreate.resourceIri + getResources + .getResourcesV2( + resourceIris = Seq(resourceIri), + requestingUser = requestingUser, + targetSchema = ApiV2Complex, + schemaOptions = SchemaOptions.ForStandoffWithTextValues, + ) + .mapError { case _: NotFoundException => + UpdateNotPerformedException( + s"Resource <$resourceIri> was not created. Please report this as a possible bug.", + ) + } + } - val resourceFuture: Task[ReadResourcesSequenceV2] = for { - resourcesResponse <- getResources.getResourcesV2( - resourceIris = Seq(resourceIri), - requestingUser = requestingUser, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - ) - - resource: ReadResourceV2 = resourcesResponse.toResource(requestedResourceIri = resourceIri) + private def generateSparqlToCreateMultipleValuesV2( + resourceIri: IRI, + values: Map[SmartIri, Seq[GenerateSparqlForValueInNewResourceV2]], + creationDate: Instant, + requestingUser: User, + ): Task[GenerateSparqlToCreateMultipleValuesResponseV2] = + for { + // Generate SPARQL to create links and LinkValues for standoff links in text values. + sparqlForStandoffLinks <- + generateInsertSparqlForStandoffLinksInMultipleValues( + resourceIri = resourceIri, + values = values.values.flatten, + creationDate = creationDate, + ) - _ <- ZIO.when( - resource.resourceClassIri.toString != resourceReadyToCreate.sparqlTemplateResourceToCreate.resourceClassIri, - ) { - ZIO.fail(AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong resource class")) - } + // Generate SPARQL for each value. + sparqlForPropertyValueFutures = + values.map { case (propertyIri: SmartIri, valuesToCreate: Seq[GenerateSparqlForValueInNewResourceV2]) => + val values = ZIO.foreach(valuesToCreate.zipWithIndex) { + case (valueToCreate: GenerateSparqlForValueInNewResourceV2, valueHasOrder: Int) => + generateInsertSparqlWithUnverifiedValue( + resourceIri = resourceIri, + propertyIri = propertyIri, + valueToCreate = valueToCreate, + valueHasOrder = valueHasOrder, + resourceCreationDate = creationDate, + requestingUser = requestingUser, + ) + } + propertyIri -> values + } - _ <- ZIO.when(resource.attachedToUser != requestingUser.id) { - ZIO.fail(AssertionException(s"Resource <$resourceIri> was saved, but it is attached to the wrong user")) - } + sparqlForPropertyValues <- ZioHelper.sequence(sparqlForPropertyValueFutures) - _ <- ZIO.when(resource.projectADM.id != projectIri) { - ZIO.fail(AssertionException(s"Resource <$resourceIri> was saved, but it is attached to the wrong user")) - } + // Concatenate all the generated SPARQL. + allInsertSparql: String = + sparqlForPropertyValues.values.flatten + .map(_.insertSparql) + .mkString("\n\n") + "\n\n" + sparqlForStandoffLinks.getOrElse("") - _ <- ZIO.when(resource.permissions != resourceReadyToCreate.sparqlTemplateResourceToCreate.permissions) { - ZIO.fail(AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong permissions")) - } + // Collect all the unverified values. + unverifiedValues: Map[SmartIri, Seq[UnverifiedValueV2]] = + sparqlForPropertyValues.map { case (propertyIri, unverifiedValuesWithSparql) => + propertyIri -> unverifiedValuesWithSparql.map( + _.unverifiedValue, + ) + } + } yield GenerateSparqlToCreateMultipleValuesResponseV2( + insertSparql = allInsertSparql, + unverifiedValues = unverifiedValues, + hasStandoffLink = sparqlForStandoffLinks.isDefined, + ) - // Undo any escapes in the submitted rdfs:label to compare it with the saved one. - unescapedLabel: String = Iri.fromSparqlEncodedString( - resourceReadyToCreate.sparqlTemplateResourceToCreate.resourceLabel, - ) + /** + * Represents SPARQL generated to create one of multiple values in a new resource. + * + * @param insertSparql the generated SPARQL. + * @param unverifiedValue an [[UnverifiedValueV2]] representing the value that is to be created. + */ + private case class InsertSparqlWithUnverifiedValue(insertSparql: String, unverifiedValue: UnverifiedValueV2) - _ <- ZIO.when(resource.label != unescapedLabel) { - ZIO.fail(AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong label")) - } + /** + * Generates SPARQL to create one of multiple values in a new resource. + * + * @param resourceIri the IRI of the resource. + * @param propertyIri the IRI of the property that will point to the value. + * @param valueToCreate the value to be created. + * @param valueHasOrder the value's `knora-base:valueHasOrder`. + * @param resourceCreationDate the creation date of the resource. + * @param requestingUser the user making the request. + * @return a [[InsertSparqlWithUnverifiedValue]] containing the generated SPARQL and an [[UnverifiedValueV2]]. + */ + private def generateInsertSparqlWithUnverifiedValue( + resourceIri: IRI, + propertyIri: SmartIri, + valueToCreate: GenerateSparqlForValueInNewResourceV2, + valueHasOrder: Int, + resourceCreationDate: Instant, + requestingUser: User, + ): Task[InsertSparqlWithUnverifiedValue] = + for { + // Make new value UUID. + newValueUUID <- + ValuesResponderV2Live.makeNewValueUUID(valueToCreate.customValueIri, valueToCreate.customValueUUID) + newValueIri <- + iriService.checkOrCreateEntityIri( + valueToCreate.customValueIri, + stringFormatter.makeRandomValueIri(resourceIri, Some(newValueUUID)), + ) - savedPropertyIris: Set[SmartIri] = resource.values.keySet + // Make a creation date for the value. If a custom creation date is given for a value, consider that otherwise + // use resource creation date for the value. + valueCreationDate: Instant = valueToCreate.customValueCreationDate.getOrElse(resourceCreationDate) - // Check that the property knora-base:hasStandoffLinkToValue was automatically added if necessary. - expectedPropertyIris: Set[SmartIri] = - resourceReadyToCreate.values.keySet ++ (if (resourceReadyToCreate.hasStandoffLink) { - Some(OntologyConstants.KnoraBase.HasStandoffLinkToValue.toSmartIri) - } else { None }) + // Generate the SPARQL. + insertSparql: String = + valueToCreate.valueContent match { + case linkValueContentV2: LinkValueContentV2 => + // We're creating a link. + + // Construct a SparqlTemplateLinkUpdate to tell the SPARQL template how to create + // the link and its LinkValue. + val sparqlTemplateLinkUpdate = SparqlTemplateLinkUpdate( + linkPropertyIri = propertyIri.fromLinkValuePropToLinkProp, + directLinkExists = false, + insertDirectLink = true, + deleteDirectLink = false, + linkValueExists = false, + linkTargetExists = linkValueContentV2.referredResourceExists, + newLinkValueIri = newValueIri, + linkTargetIri = linkValueContentV2.referredResourceIri, + currentReferenceCount = 0, + newReferenceCount = 1, + newLinkValueCreator = requestingUser.id, + newLinkValuePermissions = valueToCreate.permissions, + ) - _ <- ZIO.when(savedPropertyIris != expectedPropertyIris) { - val msg = - s"Resource <$resourceIri> was saved, but it has the wrong properties: expected (${expectedPropertyIris - .map(_.toSparql) - .mkString(", ")}), but saved (${savedPropertyIris.map(_.toSparql).mkString(", ")})" - ZIO.fail(AssertionException(msg)) - } + // Generate SPARQL for the link. + sparql.v2.txt + .generateInsertStatementsForCreateLink( + resourceIri = resourceIri, + linkUpdate = sparqlTemplateLinkUpdate, + creationDate = valueCreationDate, + newValueUUID = newValueUUID, + maybeComment = valueToCreate.valueContent.comment, + maybeValueHasOrder = Some(valueHasOrder), + ) + .toString() + + case otherValueContentV2 => + // We're creating an ordinary value. Generate SPARQL for it. + sparql.v2.txt + .generateInsertStatementsForCreateValue( + resourceIri = resourceIri, + propertyIri = propertyIri, + value = otherValueContentV2, + newValueIri = newValueIri, + newValueUUID = newValueUUID, + linkUpdates = Seq.empty[ + SparqlTemplateLinkUpdate, + ], // This is empty because we have to generate SPARQL for standoff links separately. + valueCreator = requestingUser.id, + valuePermissions = valueToCreate.permissions, + creationDate = valueCreationDate, + maybeValueHasOrder = Some(valueHasOrder), + ) + .toString() + } + } yield InsertSparqlWithUnverifiedValue( + insertSparql = insertSparql, + unverifiedValue = UnverifiedValueV2( + newValueIri = newValueIri, + newValueUUID = newValueUUID, + valueContent = valueToCreate.valueContent.unescape, + permissions = valueToCreate.permissions, + creationDate = valueCreationDate, + ), + ) - // Ignore knora-base:hasStandoffLinkToValue when checking the expected values. - _ <- ZIO.foreachDiscard(resource.values - OntologyConstants.KnoraBase.HasStandoffLinkToValue.toSmartIri) { - case (propertyIri: SmartIri, savedValues: Seq[ReadValueV2]) => - val expectedValues: Seq[UnverifiedValueV2] = resourceReadyToCreate.values(propertyIri) - for { - _ <- ZIO.when(expectedValues.size != savedValues.size) { - ZIO.fail(AssertionException(s"Resource <$resourceIri> was saved, but it has the wrong values")) - } - - _ <- ZIO.foreachDiscard(savedValues.zip(expectedValues)) { case (savedValue, expectedValue) => - ZIO.when( - !(expectedValue.valueContent.wouldDuplicateCurrentVersion(savedValue.valueContent) && - savedValue.permissions == expectedValue.permissions && - savedValue.attachedToUser == requestingUser.id), - ) { - val msg = s"Resource <$resourceIri> was saved, but one or more of its values are not correct" - ZIO.fail(AssertionException(msg)) - } - } - } yield () - } - } yield ReadResourcesSequenceV2(resources = Seq(resource.copy(values = Map.empty))) + private def generateInsertSparqlForStandoffLinksInMultipleValues( + resourceIri: IRI, + values: Iterable[GenerateSparqlForValueInNewResourceV2], + creationDate: Instant, + ): Task[Option[String]] = { + // To create LinkValues for the standoff links in the values to be created, we need to compute + // the initial reference count of each LinkValue. This is equal to the number of TextValues in the resource + // that have standoff links to a particular target resource. + + // First, get the standoff link targets from all the text values to be created. + val standoffLinkTargetsPerTextValue: Vector[Set[IRI]] = + values.foldLeft(Vector.empty[Set[IRI]]) { + case (standoffLinkTargetsAcc: Vector[Set[IRI]], createValueV2: GenerateSparqlForValueInNewResourceV2) => + createValueV2.valueContent match { + case textValueContentV2: TextValueContentV2 + if textValueContentV2.standoffLinkTagTargetResourceIris.nonEmpty => + standoffLinkTargetsAcc :+ textValueContentV2.standoffLinkTagTargetResourceIris + + case _ => standoffLinkTargetsAcc + } + } - resourceFuture.mapError { case _: NotFoundException => - UpdateNotPerformedException( - s"Resource <$resourceIri> was not created. Please report this as a possible bug.", - ) + if (standoffLinkTargetsPerTextValue.nonEmpty) { + // Combine those resource references into a single list, so if there are n text values with a link to + // some IRI, the list will contain that IRI n times. + val allStandoffLinkTargets: Vector[IRI] = standoffLinkTargetsPerTextValue.flatten + + // Now we need to count the number of times each IRI occurs in allStandoffLinkTargets. To do this, first + // use groupBy(identity). The groupBy method takes a function that returns a key for each item in the + // collection, and makes a Map in which items with the same key are grouped together. The identity + // function just returns its argument. So groupBy(identity) makes a Map[IRI, Vector[IRI]] in which each + // IRI points to a sequence of the same IRI repeated as many times as it occurred in allStandoffLinkTargets. + val allStandoffLinkTargetsGrouped: Map[IRI, Vector[IRI]] = allStandoffLinkTargets.groupBy(identity) + + // Replace each Vector[IRI] with its size. That's the number of text values containing + // standoff links to that IRI. + val initialReferenceCounts: Map[IRI, Int] = allStandoffLinkTargetsGrouped.view.mapValues(_.size).toMap + + // For each standoff link target IRI, construct a SparqlTemplateLinkUpdate to create a hasStandoffLinkTo property + // and one LinkValue with its initial reference count. + val standoffLinkUpdatesFutures: Seq[Task[SparqlTemplateLinkUpdate]] = initialReferenceCounts.toSeq.map { + case (targetIri, initialReferenceCount) => + for { + newValueIri <- makeUnusedValueIri(resourceIri) + } yield SparqlTemplateLinkUpdate( + linkPropertyIri = OntologyConstants.KnoraBase.HasStandoffLinkTo.toSmartIri, + directLinkExists = false, + insertDirectLink = true, + deleteDirectLink = false, + linkValueExists = false, + linkTargetExists = + true, // doesn't matter, the generateInsertStatementsForStandoffLinks template doesn't use it + newLinkValueIri = newValueIri, + linkTargetIri = targetIri, + currentReferenceCount = 0, + newReferenceCount = initialReferenceCount, + newLinkValueCreator = KnoraUserRepo.builtIn.SystemUser.id.value, + newLinkValuePermissions = standoffLinkValuePermissions, + ) + } + for { + standoffLinkUpdates <- ZIO.collectAll(standoffLinkUpdatesFutures) + // Generate SPARQL INSERT statements based on those SparqlTemplateLinkUpdates. + sparqlInsert = + sparql.v2.txt + .generateInsertStatementsForStandoffLinks( + resourceIri = resourceIri, + linkUpdates = standoffLinkUpdates, + creationDate = creationDate, + ) + .toString() + } yield Some(sparqlInsert) + } else { + ZIO.succeed(None) } } + + /** + * A convenience method for generating an unused random value IRI. + * + * @param resourceIri the IRI of the containing resource. + * @return the new value IRI. + */ + private def makeUnusedValueIri(resourceIri: IRI): Task[IRI] = + iriService.makeUnusedIri(stringFormatter.makeRandomValueIri(resourceIri)) + + /** + * The permissions that are granted by every `knora-base:LinkValue` describing a standoff link. + */ + private lazy val standoffLinkValuePermissions: String = { + val permissions: Set[PermissionADM] = Set( + PermissionADM.from(Permission.ObjectAccess.ChangeRights, KnoraUserRepo.builtIn.SystemUser.id.value), + PermissionADM.from(Permission.ObjectAccess.View, KnoraGroupRepo.builtIn.UnknownUser.id.value), + ) + + PermissionUtilADM.formatPermissionADMs(permissions, PermissionType.OAP) + } + } diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResources.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResource.scala.txt similarity index 66% rename from webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResources.scala.txt rename to webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResource.scala.txt index 168a1ae195..647db7fcf2 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResources.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/createNewResource.scala.txt @@ -17,7 +17,7 @@ * @param creatorIri the IRI of the creator of the resources. *@ @(dataNamedGraph: IRI, - resourcesToCreate: Seq[SparqlTemplateResourceToCreate], + resourceToCreate: SparqlTemplateResourceToCreate, projectIri: IRI, creatorIri: IRI) @@ -29,16 +29,14 @@ PREFIX knora-base: INSERT DATA { GRAPH <@dataNamedGraph> { - @for(res <- resourcesToCreate) { - <@res.resourceIri> rdf:type <@res.resourceClassIri> ; - knora-base:isDeleted false ; - knora-base:attachedToUser <@creatorIri> ; - knora-base:attachedToProject <@projectIri> ; - rdfs:label """@res.resourceLabel""" ; - knora-base:hasPermissions "@res.permissions" ; - knora-base:creationDate "@res.resourceCreationDate"^^xsd:dateTime . + <@resourceToCreate.resourceIri> rdf:type <@resourceToCreate.resourceClassIri> ; + knora-base:isDeleted false ; + knora-base:attachedToUser <@creatorIri> ; + knora-base:attachedToProject <@projectIri> ; + rdfs:label """@resourceToCreate.resourceLabel""" ; + knora-base:hasPermissions "@resourceToCreate.permissions" ; + knora-base:creationDate "@resourceToCreate.resourceCreationDate"^^xsd:dateTime . - @res.sparqlForValues - } + @resourceToCreate.sparqlForValues } }