From faa2e552c2c93d71874518caa6763bf02dbbb6c0 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Fri, 7 Feb 2020 17:52:41 +0100 Subject: [PATCH] fix(api-v2): Change link value comment (#1582) --- .../org/knora/webapi/SharedTestDataADM.scala | 111 ++- .../responders/v2/ValuesResponderV2.scala | 760 ++++++++++-------- .../sparql/v2/changeLinkMetadata.scala.txt | 147 ++++ ...k.scala.txt => changeLinkTarget.scala.txt} | 0 .../webapi/e2e/v2/ValuesRouteV2E2ESpec.scala | 432 ++++++++-- .../responders/v2/ValuesResponderV2Spec.scala | 615 +++++++++++--- 6 files changed, 1492 insertions(+), 573 deletions(-) create mode 100644 webapi/src/main/twirl/queries/sparql/v2/changeLinkMetadata.scala.txt rename webapi/src/main/twirl/queries/sparql/v2/{changeLink.scala.txt => changeLinkTarget.scala.txt} (100%) diff --git a/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala b/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala index 3a8c1431a6..7badf8c77f 100644 --- a/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala @@ -1005,22 +1005,44 @@ object SharedTestDataADM { } def createLinkValueRequest(resourceIri: IRI, - targetResourceIri: IRI): String = { - s"""{ - | "@id" : "$resourceIri", - | "@type" : "anything:Thing", - | "anything:hasOtherThingValue" : { - | "@type" : "knora-api:LinkValue", - | "knora-api:linkValueHasTargetIri" : { - | "@id" : "$targetResourceIri" - | } - | }, - | "@context" : { - | "xsd" : "http://www.w3.org/2001/XMLSchema#", - | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", - | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" - | } - |}""".stripMargin + targetResourceIri: IRI, + valueHasComment: Option[String] = None): String = { + valueHasComment match { + case Some(comment) => + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasOtherThingValue" : { + | "@type" : "knora-api:LinkValue", + | "knora-api:linkValueHasTargetIri" : { + | "@id" : "$targetResourceIri" + | }, + | "knora-api:valueHasComment" : "$comment" + | }, + | "@context" : { + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + + case None => + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasOtherThingValue" : { + | "@type" : "knora-api:LinkValue", + | "knora-api:linkValueHasTargetIri" : { + | "@id" : "$targetResourceIri" + | } + | }, + | "@context" : { + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + } } def updateIntValueRequest(resourceIri: IRI, @@ -1430,23 +1452,46 @@ object SharedTestDataADM { def updateLinkValueRequest(resourceIri: IRI, valueIri: IRI, - targetResourceIri: IRI): String = { - s"""{ - | "@id" : "$resourceIri", - | "@type" : "anything:Thing", - | "anything:hasOtherThingValue" : { - | "@id" : "$valueIri", - | "@type" : "knora-api:LinkValue", - | "knora-api:linkValueHasTargetIri" : { - | "@id" : "$targetResourceIri" - | } - | }, - | "@context" : { - | "xsd" : "http://www.w3.org/2001/XMLSchema#", - | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", - | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" - | } - |}""".stripMargin + targetResourceIri: IRI, + comment: Option[String] = None): String = { + comment match { + case Some(definedComment) => + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasOtherThingValue" : { + | "@id" : "$valueIri", + | "@type" : "knora-api:LinkValue", + | "knora-api:linkValueHasTargetIri" : { + | "@id" : "$targetResourceIri" + | }, + | "knora-api:valueHasComment" : "$definedComment" + | }, + | "@context" : { + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + + case None => + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasOtherThingValue" : { + | "@id" : "$valueIri", + | "@type" : "knora-api:LinkValue", + | "knora-api:linkValueHasTargetIri" : { + | "@id" : "$targetResourceIri" + | } + | }, + | "@context" : { + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + } } def updateStillImageFileValueRequest(resourceIri: IRI, 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 5b32e41d7c..bb28277829 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 @@ -43,21 +43,21 @@ import org.knora.webapi.util.{PermissionUtilADM, SmartIri} import scala.concurrent.Future /** - * Handles requests to read and write Knora values. - */ + * Handles requests to read and write Knora values. + */ class ValuesResponderV2(responderData: ResponderData) extends Responder(responderData) { /** - * The IRI and content of a new value or value version whose existence in the triplestore has been verified. - * - * @param newValueIri the IRI that was assigned to the new value. - * @param value the content of the new value. - */ + * The IRI and content of a new value or value version whose existence in the triplestore has been verified. + * + * @param newValueIri the IRI that was assigned to the new value. + * @param value the content of the new value. + */ case class VerifiedValueV2(newValueIri: IRI, value: ValueContentV2, permissions: String) /** - * Receives a message of type [[ValuesResponderRequestV2]], and returns an appropriate response message. - */ + * Receives a message of type [[ValuesResponderRequestV2]], and returns an appropriate response message. + */ def receive(msg: ValuesResponderRequestV2) = msg match { case createValueRequest: CreateValueRequestV2 => createValueV2(createValueRequest) case updateValueRequest: UpdateValueRequestV2 => updateValueV2(updateValueRequest) @@ -67,11 +67,11 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Creates a new value in an existing resource. - * - * @param createValueRequest the request to create the value. - * @return a [[CreateValueResponseV2]]. - */ + * Creates a new value in an existing resource. + * + * @param createValueRequest the request to create the value. + * @return a [[CreateValueResponseV2]]. + */ private def createValueV2(createValueRequest: CreateValueRequestV2): Future[CreateValueResponseV2] = { def makeTaskFuture: Future[CreateValueResponseV2] = { for { @@ -301,20 +301,20 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Creates a new value (either an ordinary value or a link), using an existing transaction, assuming that - * pre-update checks have already been done. - * - * @param dataNamedGraph the named graph in which the value is to be created. - * @param projectIri the IRI of the project in which to create the value. - * @param resourceInfo information about the the resource in which to create the value. - * @param propertyIri the IRI of the property that will point from the resource to the value, or, if the - * value is a link value, the IRI of the link property. - * @param value the value to create. - * @param valueCreator the IRI of the new value's owner. - * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return an [[UnverifiedValueV2]]. - */ + * Creates a new value (either an ordinary value or a link), using an existing transaction, assuming that + * pre-update checks have already been done. + * + * @param dataNamedGraph the named graph in which the value is to be created. + * @param projectIri the IRI of the project in which to create the value. + * @param resourceInfo information about the the resource in which to create the value. + * @param propertyIri the IRI of the property that will point from the resource to the value, or, if the + * value is a link value, the IRI of the link property. + * @param value the value to create. + * @param valueCreator the IRI of the new value's owner. + * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return an [[UnverifiedValueV2]]. + */ private def createValueV2AfterChecks(dataNamedGraph: IRI, projectIri: IRI, resourceInfo: ReadResourceV2, @@ -349,16 +349,16 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Creates an ordinary value (i.e. not a link), using an existing transaction, assuming that pre-update checks have already been done. - * - * @param resourceInfo information about the the resource in which to create the value. - * @param propertyIri the property that should point to the value. - * @param value an [[ValueContentV2]] describing the value. - * @param valueCreator the IRI of the new value's owner. - * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return an [[UnverifiedValueV2]]. - */ + * Creates an ordinary value (i.e. not a link), using an existing transaction, assuming that pre-update checks have already been done. + * + * @param resourceInfo information about the the resource in which to create the value. + * @param propertyIri the property that should point to the value. + * @param value an [[ValueContentV2]] describing the value. + * @param valueCreator the IRI of the new value's owner. + * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return an [[UnverifiedValueV2]]. + */ private def createOrdinaryValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, propertyIri: SmartIri, @@ -422,17 +422,17 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Creates a link, using an existing transaction, assuming that pre-update checks have already been done. - * - * @param dataNamedGraph the named graph in which the link is to be created. - * @param resourceInfo information about the the resource in which to create the value. - * @param linkPropertyIri the link property. - * @param linkValueContent a [[LinkValueContentV2]] specifying the target resource. - * @param valueCreator the IRI of the new link value's owner. - * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return an [[UnverifiedValueV2]]. - */ + * Creates a link, using an existing transaction, assuming that pre-update checks have already been done. + * + * @param dataNamedGraph the named graph in which the link is to be created. + * @param resourceInfo information about the the resource in which to create the value. + * @param linkPropertyIri the link property. + * @param linkValueContent a [[LinkValueContentV2]] specifying the target resource. + * @param valueCreator the IRI of the new link value's owner. + * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return an [[UnverifiedValueV2]]. + */ private def createLinkValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, linkPropertyIri: SmartIri, @@ -480,20 +480,20 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * 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. - */ + * 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. - */ + * 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 generateSparqToCreateMultipleValuesV2(createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2): Future[GenerateSparqlToCreateMultipleValuesResponseV2] = { for { // Generate SPARQL to create links and LinkValues for standoff links in text values. @@ -529,16 +529,16 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * 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 creationDate the timestamp to be used as the value creation time. - * @param requestingUser the user making the request. - * @return a [[InsertSparqlWithUnverifiedValue]] containing the generated SPARQL and an [[UnverifiedValueV2]]. - */ + * 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 creationDate the timestamp to be used as the value creation time. + * @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, @@ -608,11 +608,11 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * 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. - */ + * 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): 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 @@ -672,39 +672,39 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Creates a new version of an existing value. - * - * @param updateValueRequest the request to update the value. - * @return a [[UpdateValueResponseV2]]. - */ + * Creates a new version of an existing value. + * + * @param updateValueRequest the request to update the value. + * @return a [[UpdateValueResponseV2]]. + */ private def updateValueV2(updateValueRequest: UpdateValueRequestV2): Future[UpdateValueResponseV2] = { /** - * Information about a resource, a submitted property, and a value of the property. - * - * @param resource the contents of the resource. - * @param submittedInternalPropertyIri the internal IRI of the submitted property. - * @param adjustedInternalPropertyInfo the internal definition of the submitted property, adjusted - * as follows: an adjusted version of the submitted property: - * if it's a link value property, substitute the - * corresponding link property. - * @param value the requested value. - */ + * Information about a resource, a submitted property, and a value of the property. + * + * @param resource the contents of the resource. + * @param submittedInternalPropertyIri the internal IRI of the submitted property. + * @param adjustedInternalPropertyInfo the internal definition of the submitted property, adjusted + * as follows: an adjusted version of the submitted property: + * if it's a link value property, substitute the + * corresponding link property. + * @param value the requested value. + */ case class ResourcePropertyValue(resource: ReadResourceV2, submittedInternalPropertyIri: SmartIri, adjustedInternalPropertyInfo: ReadPropertyInfoV2, value: ReadValueV2) /** - * Gets information about a resource, a submitted property, and a value of the property, and does - * some checks to see if the submitted information is correct. - * - * @param resourceIri the IRI of the resource. - * @param submittedExternalResourceClassIri the submitted external IRI of the resource class. - * @param submittedExternalPropertyIri the submitted external IRI of the property. - * @param valueIri the IRI of the value. - * @param submittedExternalValueType the submitted external IRI of the value type. - * @return a [[ResourcePropertyValue]]. - */ + * Gets information about a resource, a submitted property, and a value of the property, and does + * some checks to see if the submitted information is correct. + * + * @param resourceIri the IRI of the resource. + * @param submittedExternalResourceClassIri the submitted external IRI of the resource class. + * @param submittedExternalPropertyIri the submitted external IRI of the property. + * @param valueIri the IRI of the value. + * @param submittedExternalValueType the submitted external IRI of the value type. + * @return a [[ResourcePropertyValue]]. + */ def getResourcePropertyValue(resourceIri: IRI, submittedExternalResourceClassIri: SmartIri, submittedExternalPropertyIri: SmartIri, @@ -776,11 +776,11 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Updates the permissions attached to a value. - * - * @param updateValuePermissionsV2 the update request. - * @return an [[UpdateValueResponseV2]]. - */ + * Updates the permissions attached to a value. + * + * @param updateValuePermissionsV2 the update request. + * @return an [[UpdateValueResponseV2]]. + */ def makeTaskFutureToUpdateValuePermissions(updateValuePermissionsV2: UpdateValuePermissionsV2): Future[UpdateValueResponseV2] = { for { // Do the initial checks, and get information about the resource, the property, and the value. @@ -860,11 +860,11 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Updates the contents of a value. - * - * @param updateValueContentV2 the update request. - * @return an [[UpdateValueResponseV2]]. - */ + * Updates the contents of a value. + * + * @param updateValueContentV2 the update request. + * @return an [[UpdateValueResponseV2]]. + */ def makeTaskFutureToUpdateValueContent(updateValueContentV2: UpdateValueContentV2): Future[UpdateValueResponseV2] = { for { // Do the initial checks, and get information about the resource, the property, and the value. @@ -1029,20 +1029,20 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Updates an existing value (either an ordinary value or a link), using an existing transaction, assuming that - * pre-update checks have already been done. - * - * @param dataNamedGraph the named graph in which the value is to be created. - * @param resourceInfo information about the the resource in which to create the value. - * @param propertyIri the IRI of the property that will point from the resource to the value, or, if the - * value is a link value, the IRI of the link property. - * @param currentValue the value to be updated. - * @param newValueVersion the new version of the value. - * @param valueCreator the IRI of the new value's owner. - * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return an [[UnverifiedValueV2]]. - */ + * Updates an existing value (either an ordinary value or a link), using an existing transaction, assuming that + * pre-update checks have already been done. + * + * @param dataNamedGraph the named graph in which the value is to be created. + * @param resourceInfo information about the the resource in which to create the value. + * @param propertyIri the IRI of the property that will point from the resource to the value, or, if the + * value is a link value, the IRI of the link property. + * @param currentValue the value to be updated. + * @param newValueVersion the new version of the value. + * @param valueCreator the IRI of the new value's owner. + * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return an [[UnverifiedValueV2]]. + */ private def updateValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, propertyIri: SmartIri, @@ -1082,19 +1082,19 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Changes an ordinary value (i.e. not a link), assuming that pre-update checks have already been done. - * - * @param dataNamedGraph the IRI of the named graph to be updated. - * @param resourceInfo information about the resource containing the value. - * @param propertyIri the IRI of the property that points to the value. - * @param currentValue a [[ReadValueV2]] representing the existing value version. - * @param newValueIri the IRI of the new value. - * @param newValueVersion a [[ValueContentV2]] representing the new value version, in the internal schema. - * @param valueCreator the IRI of the new value's owner. - * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return an [[UnverifiedValueV2]]. - */ + * Changes an ordinary value (i.e. not a link), assuming that pre-update checks have already been done. + * + * @param dataNamedGraph the IRI of the named graph to be updated. + * @param resourceInfo information about the resource containing the value. + * @param propertyIri the IRI of the property that points to the value. + * @param currentValue a [[ReadValueV2]] representing the existing value version. + * @param newValueIri the IRI of the new value. + * @param newValueVersion a [[ValueContentV2]] representing the new value version, in the internal schema. + * @param valueCreator the IRI of the new value's owner. + * @param valuePermissions the literal that should be used as the object of the new value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return an [[UnverifiedValueV2]]. + */ private def updateOrdinaryValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, propertyIri: SmartIri, @@ -1185,18 +1185,18 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Changes a link, assuming that pre-update checks have already been done. - * - * @param dataNamedGraph the IRI of the named graph to be updated. - * @param resourceInfo information about the resource containing the link. - * @param linkPropertyIri the IRI of the link property. - * @param currentLinkValue a [[LinkValueContentV2]] representing the `knora-base:LinkValue` for the existing link. - * @param newLinkValue a [[LinkValueContentV2]] indicating the new target resource. - * @param valueCreator the IRI of the new link value's owner. - * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return an [[UnverifiedValueV2]]. - */ + * Changes a link, assuming that pre-update checks have already been done. + * + * @param dataNamedGraph the IRI of the named graph to be updated. + * @param resourceInfo information about the resource containing the link. + * @param linkPropertyIri the IRI of the link property. + * @param currentLinkValue a [[LinkValueContentV2]] representing the `knora-base:LinkValue` for the existing link. + * @param newLinkValue a [[LinkValueContentV2]] indicating the new target resource. + * @param valueCreator the IRI of the new link value's owner. + * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return an [[UnverifiedValueV2]]. + */ private def updateLinkValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, linkPropertyIri: SmartIri, @@ -1205,63 +1205,100 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde valueCreator: IRI, valuePermissions: String, requestingUser: UserADM): Future[UnverifiedValueV2] = { - // Delete the existing link and decrement its LinkValue's reference count. - val sparqlTemplateLinkUpdateForCurrentLink: SparqlTemplateLinkUpdate = decrementLinkValue( - sourceResourceInfo = resourceInfo, - linkPropertyIri = linkPropertyIri, - targetResourceIri = currentLinkValue.referredResourceIri, - valueCreator = valueCreator, - valuePermissions = valuePermissions, - requestingUser = requestingUser - ) - // Create a new link, and create a new LinkValue for it. - val sparqlTemplateLinkUpdateForNewLink: SparqlTemplateLinkUpdate = incrementLinkValue( - sourceResourceInfo = resourceInfo, - linkPropertyIri = linkPropertyIri, - targetResourceIri = newLinkValue.referredResourceIri, - valueCreator = valueCreator, - valuePermissions = valuePermissions, - requestingUser = requestingUser - ) + // Are we changing the link target? + if (currentLinkValue.referredResourceIri != newLinkValue.referredResourceIri) { + // Delete the existing link and decrement its LinkValue's reference count. + val sparqlTemplateLinkUpdateForCurrentLink: SparqlTemplateLinkUpdate = decrementLinkValue( + sourceResourceInfo = resourceInfo, + linkPropertyIri = linkPropertyIri, + targetResourceIri = currentLinkValue.referredResourceIri, + valueCreator = valueCreator, + valuePermissions = valuePermissions, + requestingUser = requestingUser + ) - // Make a timestamp to indicate when the link value was updated. - val currentTime: Instant = Instant.now + // Create a new link, and create a new LinkValue for it. + val sparqlTemplateLinkUpdateForNewLink: SparqlTemplateLinkUpdate = incrementLinkValue( + sourceResourceInfo = resourceInfo, + linkPropertyIri = linkPropertyIri, + targetResourceIri = newLinkValue.referredResourceIri, + valueCreator = valueCreator, + valuePermissions = valuePermissions, + requestingUser = requestingUser + ) - for { - // Generate a SPARQL update string. - sparqlUpdate <- Future(queries.sparql.v2.txt.changeLink( - dataNamedGraph = dataNamedGraph, - triplestore = settings.triplestoreType, - linkSourceIri = resourceInfo.resourceIri, - linkUpdateForCurrentLink = sparqlTemplateLinkUpdateForCurrentLink, - linkUpdateForNewLink = sparqlTemplateLinkUpdateForNewLink, - maybeComment = newLinkValue.comment, - currentTime = currentTime, - requestingUser = requestingUser.id, - stringFormatter = stringFormatter - ).toString()) + // Make a timestamp to indicate when the link value was updated. + val currentTime: Instant = Instant.now - /* - _ = println("================ Update link ================") - _ = println(sparqlUpdate) - _ = println("==============================================") - */ + for { + // Generate a SPARQL update string. + sparqlUpdate <- Future(queries.sparql.v2.txt.changeLinkTarget( + dataNamedGraph = dataNamedGraph, + triplestore = settings.triplestoreType, + linkSourceIri = resourceInfo.resourceIri, + linkUpdateForCurrentLink = sparqlTemplateLinkUpdateForCurrentLink, + linkUpdateForNewLink = sparqlTemplateLinkUpdateForNewLink, + maybeComment = newLinkValue.comment, + currentTime = currentTime, + requestingUser = requestingUser.id, + stringFormatter = stringFormatter + ).toString()) - _ <- (storeManager ? SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse] - } yield UnverifiedValueV2( - newValueIri = sparqlTemplateLinkUpdateForNewLink.newLinkValueIri, - valueContent = newLinkValue.unescape, - permissions = valuePermissions, - creationDate = currentTime - ) + /* + _ = println("================ Update link ================") + _ = println(sparqlUpdate) + _ = println("==============================================") + */ + + _ <- (storeManager ? SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse] + } yield UnverifiedValueV2( + newValueIri = sparqlTemplateLinkUpdateForNewLink.newLinkValueIri, + valueContent = newLinkValue.unescape, + permissions = valuePermissions, + creationDate = currentTime + ) + } else { + // We're not changing the link target, just the metadata on the LinkValue. + + val sparqlTemplateLinkUpdate: SparqlTemplateLinkUpdate = changeLinkValueMetadata( + sourceResourceInfo = resourceInfo, + linkPropertyIri = linkPropertyIri, + targetResourceIri = currentLinkValue.referredResourceIri, + valueCreator = valueCreator, + valuePermissions = valuePermissions, + requestingUser = requestingUser + ) + + // Make a timestamp to indicate when the link value was updated. + val currentTime: Instant = Instant.now + + for { + sparqlUpdate <- Future(queries.sparql.v2.txt.changeLinkMetadata( + dataNamedGraph = dataNamedGraph, + triplestore = settings.triplestoreType, + linkSourceIri = resourceInfo.resourceIri, + linkUpdate = sparqlTemplateLinkUpdate, + maybeComment = newLinkValue.comment, + currentTime = currentTime, + requestingUser = requestingUser.id + ).toString()) + + _ <- (storeManager ? SparqlUpdateRequest(sparqlUpdate)).mapTo[SparqlUpdateResponse] + } yield UnverifiedValueV2( + newValueIri = sparqlTemplateLinkUpdate.newLinkValueIri, + valueContent = newLinkValue.unescape, + permissions = valuePermissions, + creationDate = currentTime + ) + } } /** - * Marks a value as deleted. - * - * @param deleteValueRequest the request to mark the value as deleted. - */ + * Marks a value as deleted. + * + * @param deleteValueRequest the request to mark the value as deleted. + */ private def deleteValueV2(deleteValueRequest: DeleteValueRequestV2): Future[SuccessResponseV2] = { def makeTaskFuture: Future[SuccessResponseV2] = { for { @@ -1413,17 +1450,17 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde /** - * Deletes a value (either an ordinary value or a link), using an existing transaction, assuming that - * pre-update checks have already been done. - * - * @param dataNamedGraph the named graph in which the value is to be deleted. - * @param resourceInfo information about the the resource in which to create the value. - * @param propertyIri the IRI of the property that points from the resource to the value. - * @param currentValue the value to be deleted. - * @param deleteComment an optional comment explaining why the value is being deleted. - * @param requestingUser the user making the request. - * @return the IRI of the value that was marked as deleted. - */ + * Deletes a value (either an ordinary value or a link), using an existing transaction, assuming that + * pre-update checks have already been done. + * + * @param dataNamedGraph the named graph in which the value is to be deleted. + * @param resourceInfo information about the the resource in which to create the value. + * @param propertyIri the IRI of the property that points from the resource to the value. + * @param currentValue the value to be deleted. + * @param deleteComment an optional comment explaining why the value is being deleted. + * @param requestingUser the user making the request. + * @return the IRI of the value that was marked as deleted. + */ private def deleteValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, propertyIri: SmartIri, @@ -1454,16 +1491,16 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Deletes a link after checks. - * - * @param dataNamedGraph the named graph in which the value is to be deleted. - * @param resourceInfo information about the the resource in which to create the value. - * @param propertyIri the IRI of the property that points from the resource to the value. - * @param currentValue the value to be deleted. - * @param deleteComment an optional comment explaining why the value is being deleted. - * @param requestingUser the user making the request. - * @return the IRI of the value that was marked as deleted. - */ + * Deletes a link after checks. + * + * @param dataNamedGraph the named graph in which the value is to be deleted. + * @param resourceInfo information about the the resource in which to create the value. + * @param propertyIri the IRI of the property that points from the resource to the value. + * @param currentValue the value to be deleted. + * @param deleteComment an optional comment explaining why the value is being deleted. + * @param requestingUser the user making the request. + * @return the IRI of the value that was marked as deleted. + */ private def deleteLinkValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, propertyIri: SmartIri, @@ -1507,16 +1544,16 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Deletes an ordinary value after checks. - * - * @param dataNamedGraph the named graph in which the value is to be deleted. - * @param resourceInfo information about the the resource in which to create the value. - * @param propertyIri the IRI of the property that points from the resource to the value. - * @param currentValue the value to be deleted. - * @param deleteComment an optional comment explaining why the value is being deleted. - * @param requestingUser the user making the request. - * @return the IRI of the value that was marked as deleted. - */ + * Deletes an ordinary value after checks. + * + * @param dataNamedGraph the named graph in which the value is to be deleted. + * @param resourceInfo information about the the resource in which to create the value. + * @param propertyIri the IRI of the property that points from the resource to the value. + * @param currentValue the value to be deleted. + * @param deleteComment an optional comment explaining why the value is being deleted. + * @param requestingUser the user making the request. + * @return the IRI of the value that was marked as deleted. + */ private def deleteOrdinaryValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, propertyIri: SmartIri, @@ -1566,15 +1603,15 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * When a property IRI is submitted for an update, makes an adjusted version of the submitted property: - * if it's a link value property, substitutes the corresponding link property, whose objects we will need to query. - * - * @param submittedPropertyIri the submitted property IRI, in the API v2 complex schema. - * @param maybeSubmittedValueType the submitted value type, if provided, in the API v2 complex schema. - * @param propertyInfoForSubmittedProperty ontology information about the submitted property, in the internal schema. - * @param requestingUser the requesting user. - * @return ontology information about the adjusted property. - */ + * When a property IRI is submitted for an update, makes an adjusted version of the submitted property: + * if it's a link value property, substitutes the corresponding link property, whose objects we will need to query. + * + * @param submittedPropertyIri the submitted property IRI, in the API v2 complex schema. + * @param maybeSubmittedValueType the submitted value type, if provided, in the API v2 complex schema. + * @param propertyInfoForSubmittedProperty ontology information about the submitted property, in the internal schema. + * @param requestingUser the requesting user. + * @return ontology information about the adjusted property. + */ private def getAdjustedInternalPropertyInfo(submittedPropertyIri: SmartIri, maybeSubmittedValueType: Option[SmartIri], propertyInfoForSubmittedProperty: ReadPropertyInfoV2, @@ -1610,12 +1647,12 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Given a set of resource IRIs, checks that they point to Knora resources. - * If not, throws an exception. - * - * @param targetResourceIris the IRIs to be checked. - * @param requestingUser the user making the request. - */ + * Given a set of resource IRIs, checks that they point to Knora resources. + * If not, throws an exception. + * + * @param targetResourceIris the IRIs to be checked. + * @param requestingUser the user making the request. + */ private def checkResourceIris(targetResourceIris: Set[IRI], requestingUser: UserADM): Future[Unit] = { if (targetResourceIris.isEmpty) { FastFuture.successful(()) @@ -1637,17 +1674,17 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Returns a resource's metadata and its values, if any, for the specified property. If the property is a link property, the result - * will contain any objects of the corresponding link value property (link values), as well as metadata for any resources that the link property points to. - * If the property's object type is `knora-base:TextValue`, the result will contain any objects of the property (text values), as well metadata - * for any resources that are objects of `knora-base:hasStandoffLinkTo`. - * - * @param resourceIri the resource IRI. - * @param propertyInfo the property definition (in the internal schema). If the caller wants to query a link, this must be the link property, - * not the link value property. - * @param requestingUser the user making the request. - * @return a [[ReadResourceV2]] containing only the resource's metadata and its values for the specified property. - */ + * Returns a resource's metadata and its values, if any, for the specified property. If the property is a link property, the result + * will contain any objects of the corresponding link value property (link values), as well as metadata for any resources that the link property points to. + * If the property's object type is `knora-base:TextValue`, the result will contain any objects of the property (text values), as well metadata + * for any resources that are objects of `knora-base:hasStandoffLinkTo`. + * + * @param resourceIri the resource IRI. + * @param propertyInfo the property definition (in the internal schema). If the caller wants to query a link, this must be the link property, + * not the link value property. + * @param requestingUser the user making the request. + * @return a [[ReadResourceV2]] containing only the resource's metadata and its values for the specified property. + */ private def getResourceWithPropertyValues(resourceIri: IRI, propertyInfo: ReadPropertyInfoV2, requestingUser: UserADM): Future[ReadResourceV2] = { for { // Get the property's object class constraint. @@ -1681,14 +1718,14 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Verifies that a value was written correctly to the triplestore. - * - * @param resourceIri the IRI of the resource that the value belongs to. - * @param propertyIri the internal IRI of the property that points to the value. If the value is a link value, - * this is the link value property. - * @param unverifiedValue the value that should have been written to the triplestore. - * @param requestingUser the user making the request. - */ + * Verifies that a value was written correctly to the triplestore. + * + * @param resourceIri the IRI of the resource that the value belongs to. + * @param propertyIri the internal IRI of the property that points to the value. If the value is a link value, + * this is the link value property. + * @param unverifiedValue the value that should have been written to the triplestore. + * @param requestingUser the user making the request. + */ private def verifyValue(resourceIri: IRI, propertyIri: SmartIri, unverifiedValue: UnverifiedValueV2, @@ -1739,13 +1776,13 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Checks that a link value points to a resource with the correct type for the link property's object class constraint. - * - * @param linkPropertyIri the IRI of the link property. - * @param objectClassConstraint the object class constraint of the link property. - * @param linkValueContent the link value. - * @param requestingUser the user making the request. - */ + * Checks that a link value points to a resource with the correct type for the link property's object class constraint. + * + * @param linkPropertyIri the IRI of the link property. + * @param objectClassConstraint the object class constraint of the link property. + * @param linkValueContent the link value. + * @param requestingUser the user making the request. + */ private def checkLinkPropertyObjectClassConstraint(linkPropertyIri: SmartIri, objectClassConstraint: SmartIri, linkValueContent: LinkValueContentV2, requestingUser: UserADM): Future[Unit] = { for { // Get a preview of the target resource, because we only need to find out its class and whether the user has permission to view it. @@ -1781,13 +1818,13 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Checks that a non-link value has the correct type for a property's object class constraint. - * - * @param propertyIri the IRI of the property that should point to the value. - * @param objectClassConstraint the property's object class constraint. - * @param valueContent the value. - * @param requestingUser the user making the request. - */ + * Checks that a non-link value has the correct type for a property's object class constraint. + * + * @param propertyIri the IRI of the property that should point to the value. + * @param objectClassConstraint the property's object class constraint. + * @param valueContent the value. + * @param requestingUser the user making the request. + */ private def checkNonLinkPropertyObjectClassConstraint(propertyIri: SmartIri, objectClassConstraint: SmartIri, valueContent: ValueContentV2, requestingUser: UserADM): Future[Unit] = { // Is the value type the same as the property's object class constraint? if (objectClassConstraint == valueContent.valueType) { @@ -1814,13 +1851,13 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Checks that a value to be updated has the correct type for the `knora-base:objectClassConstraint` of - * the property that is supposed to point to it. - * - * @param propertyInfo the property whose object class constraint is to be checked. If the value is a link value, this is the link property. - * @param valueContent the value to be updated. - * @param requestingUser the user making the request. - */ + * Checks that a value to be updated has the correct type for the `knora-base:objectClassConstraint` of + * the property that is supposed to point to it. + * + * @param propertyInfo the property whose object class constraint is to be checked. If the value is a link value, this is the link property. + * @param valueContent the value to be updated. + * @param requestingUser the user making the request. + */ private def checkPropertyObjectClassConstraint(propertyInfo: ReadPropertyInfoV2, valueContent: ValueContentV2, requestingUser: UserADM): Future[Unit] = { for { objectClassConstraint: SmartIri <- Future(propertyInfo.entityInfoContent.requireIriObject(OntologyConstants.KnoraBase.ObjectClassConstraint.toSmartIri, throw InconsistentTriplestoreDataException(s"Property ${propertyInfo.entityInfoContent.propertyIri} has no knora-base:objectClassConstraint"))) @@ -1856,14 +1893,14 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Given a [[ReadResourceV2]], finds a link that uses the specified property and points to the specified target - * resource. - * - * @param sourceResourceInfo a [[ReadResourceV2]] describing the source of the link. - * @param linkPropertyIri the IRI of the link property. - * @param targetResourceIri the IRI of the target resource. - * @return a [[ReadLinkValueV2]] describing the link value, if found. - */ + * Given a [[ReadResourceV2]], finds a link that uses the specified property and points to the specified target + * resource. + * + * @param sourceResourceInfo a [[ReadResourceV2]] describing the source of the link. + * @param linkPropertyIri the IRI of the link property. + * @param targetResourceIri the IRI of the target resource. + * @return a [[ReadLinkValueV2]] describing the link value, if found. + */ private def findLinkValue(sourceResourceInfo: ReadResourceV2, linkPropertyIri: SmartIri, targetResourceIri: IRI): Option[ReadLinkValueV2] = { @@ -1878,27 +1915,27 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Generates a [[SparqlTemplateLinkUpdate]] to tell a SPARQL update template how to create a `LinkValue` or to - * increment the reference count of an existing `LinkValue`. This happens in two cases: - * - * - When the user creates a link. In this case, neither the link nor the `LinkValue` exist yet. The - * [[SparqlTemplateLinkUpdate]] will specify that the link should be created, and that the `LinkValue` should be - * created with a reference count of 1. - * - When a text value is updated so that its standoff markup refers to a resource that it did not previously - * refer to. Here there are two possibilities: - * - If there is currently a `knora-base:hasStandoffLinkTo` link between the source and target resources, with a - * corresponding `LinkValue`, a new version of the `LinkValue` will be made, with an incremented reference count. - * - If that link and `LinkValue` don't yet exist, they will be created, and the `LinkValue` will be given - * a reference count of 1. - * - * @param sourceResourceInfo information about the source resource. - * @param linkPropertyIri the IRI of the property that links the source resource to the target resource. - * @param targetResourceIri the IRI of the target resource. - * @param valueCreator the IRI of the new link value's owner. - * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return a [[SparqlTemplateLinkUpdate]] that can be passed to a SPARQL update template. - */ + * Generates a [[SparqlTemplateLinkUpdate]] to tell a SPARQL update template how to create a `LinkValue` or to + * increment the reference count of an existing `LinkValue`. This happens in two cases: + * + * - When the user creates a link. In this case, neither the link nor the `LinkValue` exist yet. The + * [[SparqlTemplateLinkUpdate]] will specify that the link should be created, and that the `LinkValue` should be + * created with a reference count of 1. + * - When a text value is updated so that its standoff markup refers to a resource that it did not previously + * refer to. Here there are two possibilities: + * - If there is currently a `knora-base:hasStandoffLinkTo` link between the source and target resources, with a + * corresponding `LinkValue`, a new version of the `LinkValue` will be made, with an incremented reference count. + * - If that link and `LinkValue` don't yet exist, they will be created, and the `LinkValue` will be given + * a reference count of 1. + * + * @param sourceResourceInfo information about the source resource. + * @param linkPropertyIri the IRI of the property that links the source resource to the target resource. + * @param targetResourceIri the IRI of the target resource. + * @param valueCreator the IRI of the new link value's owner. + * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return a [[SparqlTemplateLinkUpdate]] that can be passed to a SPARQL update template. + */ private def incrementLinkValue(sourceResourceInfo: ReadResourceV2, linkPropertyIri: SmartIri, targetResourceIri: IRI, @@ -1955,25 +1992,25 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * Generates a [[SparqlTemplateLinkUpdate]] to tell a SPARQL update template how to decrement the reference count - * of a `LinkValue`. This happens in two cases: - * - * - When the user deletes (or changes) a user-created link. In this case, the current reference count will be 1. - * The existing link will be removed. A new version of the `LinkValue` be made with a reference count of 0, and - * will be marked as deleted. - * - When a resource reference is removed from standoff markup on a text value, so that the text value no longer - * contains any references to that target resource. In this case, a new version of the `LinkValue` will be - * made, with a decremented reference count. If the new reference count is 0, the link will be removed and the - * `LinkValue` will be marked as deleted. - * - * @param sourceResourceInfo information about the source resource. - * @param linkPropertyIri the IRI of the property that links the source resource to the target resource. - * @param targetResourceIri the IRI of the target resource. - * @param valueCreator the IRI of the new link value's owner. - * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. - * @param requestingUser the user making the request. - * @return a [[SparqlTemplateLinkUpdate]] that can be passed to a SPARQL update template. - */ + * Generates a [[SparqlTemplateLinkUpdate]] to tell a SPARQL update template how to decrement the reference count + * of a `LinkValue`. This happens in two cases: + * + * - When the user deletes (or changes) a user-created link. In this case, the current reference count will be 1. + * The existing link will be removed. A new version of the `LinkValue` be made with a reference count of 0, and + * will be marked as deleted. + * - When a resource reference is removed from standoff markup on a text value, so that the text value no longer + * contains any references to that target resource. In this case, a new version of the `LinkValue` will be + * made, with a decremented reference count. If the new reference count is 0, the link will be removed and the + * `LinkValue` will be marked as deleted. + * + * @param sourceResourceInfo information about the source resource. + * @param linkPropertyIri the IRI of the property that links the source resource to the target resource. + * @param targetResourceIri the IRI of the target resource. + * @param valueCreator the IRI of the new link value's owner. + * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return a [[SparqlTemplateLinkUpdate]] that can be passed to a SPARQL update template. + */ private def decrementLinkValue(sourceResourceInfo: ReadResourceV2, linkPropertyIri: SmartIri, targetResourceIri: IRI, @@ -2025,8 +2062,63 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde } /** - * The permissions that are granted by every `knora-base:LinkValue` describing a standoff link. - */ + * Generates a [[SparqlTemplateLinkUpdate]] to tell a SPARQL update template how to change the metadata + * on a `LinkValue`. + * + * @param sourceResourceInfo information about the source resource. + * @param linkPropertyIri the IRI of the property that links the source resource to the target resource. + * @param targetResourceIri the IRI of the target resource. + * @param valueCreator the IRI of the new link value's owner. + * @param valuePermissions the literal that should be used as the object of the new link value's `knora-base:hasPermissions` predicate. + * @param requestingUser the user making the request. + * @return a [[SparqlTemplateLinkUpdate]] that can be passed to a SPARQL update template. + */ + private def changeLinkValueMetadata(sourceResourceInfo: ReadResourceV2, + linkPropertyIri: SmartIri, + targetResourceIri: IRI, + valueCreator: IRI, + valuePermissions: String, + requestingUser: UserADM): SparqlTemplateLinkUpdate = { + + // Check whether a LinkValue already exists for this link. + val maybeLinkValueInfo: Option[ReadLinkValueV2] = findLinkValue( + sourceResourceInfo = sourceResourceInfo, + linkPropertyIri = linkPropertyIri, + targetResourceIri = targetResourceIri + ) + + // Did we find it? + maybeLinkValueInfo match { + case Some(linkValueInfo) => + // Yes. Make a SparqlTemplateLinkUpdate. + + // Generate an IRI for the new LinkValue. + val newLinkValueIri = stringFormatter.makeRandomValueIri(sourceResourceInfo.resourceIri) + + SparqlTemplateLinkUpdate( + linkPropertyIri = linkPropertyIri, + directLinkExists = true, + insertDirectLink = false, + deleteDirectLink = false, + linkValueExists = true, + linkTargetExists = true, + newLinkValueIri = newLinkValueIri, + linkTargetIri = targetResourceIri, + currentReferenceCount = linkValueInfo.valueHasRefCount, + newReferenceCount = linkValueInfo.valueHasRefCount, + newLinkValueCreator = valueCreator, + newLinkValuePermissions = valuePermissions + ) + + case None => + // We didn't find the LinkValue. This shouldn't happen. + throw InconsistentTriplestoreDataException(s"There should be a knora-base:LinkValue describing a direct link from resource <${sourceResourceInfo.resourceIri}> to resource <$targetResourceIri> using property <$linkPropertyIri>, but it seems to be missing") + } + } + + /** + * The permissions that are granted by every `knora-base:LinkValue` describing a standoff link. + */ lazy val standoffLinkValuePermissions: String = { val permissions: Set[PermissionADM] = Set( PermissionADM.changeRightsPermission(OntologyConstants.KnoraAdmin.SystemUser), diff --git a/webapi/src/main/twirl/queries/sparql/v2/changeLinkMetadata.scala.txt b/webapi/src/main/twirl/queries/sparql/v2/changeLinkMetadata.scala.txt new file mode 100644 index 0000000000..27b4cd07f9 --- /dev/null +++ b/webapi/src/main/twirl/queries/sparql/v2/changeLinkMetadata.scala.txt @@ -0,0 +1,147 @@ +@* + * Copyright © 2015-2019 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + *@ + +@import java.time.Instant +@import org.knora.webapi._ +@import org.knora.webapi.twirl.SparqlTemplateLinkUpdate +@import org.knora.webapi.messages.v1.responder.valuemessages._ + +@** + * Changes the metadata on a LinkValue. + * + * @param dataNamedGraph the named graph in which the project stores its data. + * @param triplestore the name of the triplestore being used. + * @param linkSourceIri the resource that is the source of the link. + * @param linkUpdate a [[SparqlTemplateLinkUpdate]] specifying how to update the link value. + * @param currentTime an xsd:dateTimeStamp that will be attached to the resources. + * @param requestingUser the IRI of the user making the request. + * + * To find out whether the update succeeded, the application must query the deleted link. + *@ +@(dataNamedGraph: IRI, + triplestore: String, + linkSourceIri: IRI, + linkUpdate: SparqlTemplateLinkUpdate, + maybeComment: Option[String], + currentTime: Instant, + requestingUser: IRI) + +PREFIX rdf: +PREFIX rdfs: +PREFIX owl: +PREFIX xsd: +PREFIX knora-base: + +DELETE { + GRAPH ?dataNamedGraph { + @* Delete the link source's last modification date so we can update it. *@ + ?linkSource knora-base:lastModificationDate ?linkSourceLastModificationDate . + + @* Detach the LinkValue from the link source. *@ + @if(linkUpdate.linkValueExists) { + ?linkSource ?linkValueProperty ?currentLinkValue . + + @* Delete the UUID from the current version of the link value, because the new version will store it. *@ + ?currentLinkValue knora-base:valueHasUUID ?currentLinkUUID . + } else { + @{throw SparqlGenerationException(s"linkUpdate.linkValueExists must be true in this SPARQL template"); ()} + } + } +} +INSERT { + GRAPH ?dataNamedGraph { + @* Insert a new version of the LinkValue. *@ + ?newLinkValue rdf:type knora-base:LinkValue ; + rdf:subject ?linkSource ; + rdf:predicate ?linkProperty ; + rdf:object ?linkTarget ; + knora-base:valueHasString "@linkUpdate.linkTargetIri"^^xsd:string ; + knora-base:valueHasRefCount @linkUpdate.newReferenceCount ; + knora-base:valueCreationDate "@currentTime"^^xsd:dateTime ; + knora-base:previousValue ?currentLinkValue ; + knora-base:valueHasUUID ?currentLinkUUID ; + knora-base:isDeleted false ; + @maybeComment match { + case Some(comment) => { + knora-base:valueHasComment """@comment""" ; + } + + case None => {} + } + knora-base:attachedToUser <@linkUpdate.newLinkValueCreator> ; + knora-base:hasPermissions "@linkUpdate.newLinkValuePermissions"^^xsd:string . + + @* Attach the new LinkValue to its containing resource. *@ + ?linkSource ?linkValueProperty ?newLinkValue . + + @* Update the link source's last modification date. *@ + ?linkSource knora-base:lastModificationDate "@currentTime"^^xsd:dateTime . + } +} +@* Ensure that inference is not used in the WHERE clause of this update. *@ +@if(triplestore.startsWith("graphdb")) { + USING +} +WHERE { + BIND(IRI("@dataNamedGraph") AS ?dataNamedGraph) + BIND(IRI("@linkSourceIri") AS ?linkSource) + BIND(IRI("@linkUpdate.linkPropertyIri") AS ?linkProperty) + BIND(IRI("@{linkUpdate.linkPropertyIri}Value") AS ?linkValueProperty) + BIND(IRI("@linkUpdate.linkTargetIri") AS ?linkTarget) + BIND(IRI("@linkUpdate.newLinkValueIri") AS ?newLinkValue) + + @* Do nothing if the link source doesn't exist, is marked as deleted, or isn't a knora-base:Resource. *@ + + ?linkSource rdf:type ?linkSourceClass ; + knora-base:isDeleted false . + ?linkSourceClass rdfs:subClassOf* knora-base:Resource . + + @* Make sure a direct link exists between the two resources. *@ + + @if(linkUpdate.directLinkExists) { + ?linkSource ?linkProperty ?linkTarget . + } else { + @{throw SparqlGenerationException(s"linkUpdate.directLinkExists must be true in this SPARQL template"); ()} + } + + @* + + Make sure a knora-base:LinkValue exists describing the direct link, and has the correct reference count. + + *@ + + @if(linkUpdate.linkValueExists) { + ?linkSource ?linkValueProperty ?currentLinkValue . + ?currentLinkValue rdf:type knora-base:LinkValue ; + rdf:subject ?linkSource ; + rdf:predicate ?linkProperty ; + rdf:object ?linkTarget ; + knora-base:valueHasRefCount @linkUpdate.currentReferenceCount ; + knora-base:isDeleted false ; + knora-base:valueHasUUID ?currentLinkUUID . + } else { + @{throw SparqlGenerationException(s"linkUpdate.linkValueExists must be true in this SPARQL template"); ()} + } + + @* Get the link source's last modification date, if it has one, so we can update it. *@ + + OPTIONAL { + ?linkSource knora-base:lastModificationDate ?linkSourceLastModificationDate . + } +} diff --git a/webapi/src/main/twirl/queries/sparql/v2/changeLink.scala.txt b/webapi/src/main/twirl/queries/sparql/v2/changeLinkTarget.scala.txt similarity index 100% rename from webapi/src/main/twirl/queries/sparql/v2/changeLink.scala.txt rename to webapi/src/main/twirl/queries/sparql/v2/changeLinkTarget.scala.txt diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala index fc3c682e2a..0671a082d8 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala @@ -321,9 +321,9 @@ class ValuesRouteV2E2ESpec extends E2ESpec { hasPermissions should ===(customPermissions) } - "create a text value without standoff" in { + "create a text value without standoff and without a comment" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri - val valueAsString: String = "Comment 1a" + val valueAsString: String = "text without standoff" val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) @@ -353,6 +353,194 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val savedValueAsString: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueAsString) savedValueAsString should ===(valueAsString) } + + "not update a text value without a comment without changing it" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val valueAsString: String = "text without standoff" + + val jsonLDEntity = SharedTestDataADM.updateTextValueWithoutStandoffRequest( + resourceIri = resourceIri, + valueIri = textValueWithoutStandoffIri.get, + valueAsString = valueAsString + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + assert(response.status == StatusCodes.BadRequest, response.toString) + } + + "not update a text value so it's empty" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val valueAsString: String = "" + + val jsonLDEntity = SharedTestDataADM.updateTextValueWithoutStandoffRequest( + resourceIri = resourceIri, + valueIri = textValueWithoutStandoffIri.get, + valueAsString = valueAsString + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.BadRequest, response.toString) + } + + "update a text value without standoff" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val valueAsString: String = "text without standoff updated" + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLDEntity = SharedTestDataADM.updateTextValueWithoutStandoffRequest( + resourceIri = resourceIri, + valueIri = textValueWithoutStandoffIri.get, + valueAsString = valueAsString + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + textValueWithoutStandoffIri.set(valueIri) + val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) + valueType should ===(OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = textValueWithoutStandoffIri.get, + userEmail = anythingUserEmail + ) + + val savedValueAsString: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueAsString) + savedValueAsString should ===(valueAsString) + } + + "update a text value without standoff, adding a comment" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val valueAsString: String = "text without standoff updated" + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLDEntity = SharedTestDataADM.updateTextValueWithCommentRequest( + resourceIri = resourceIri, + valueIri = textValueWithoutStandoffIri.get, + valueAsString = valueAsString, + valueHasComment = "Adding a comment" + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + textValueWithoutStandoffIri.set(valueIri) + val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) + valueType should ===(OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = textValueWithoutStandoffIri.get, + userEmail = anythingUserEmail + ) + + val savedValueAsString: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueAsString) + savedValueAsString should ===(valueAsString) + } + + "not update a text value without standoff and with a comment without changing it" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val valueAsString: String = "text without standoff updated" + + val jsonLDEntity = SharedTestDataADM.updateTextValueWithCommentRequest( + resourceIri = resourceIri, + valueIri = textValueWithoutStandoffIri.get, + valueAsString = valueAsString, + valueHasComment = "Adding a comment" + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.BadRequest, response.toString) + } + + "update a text value without standoff, changing only the a comment" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val valueAsString: String = "text without standoff updated" + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLDEntity = SharedTestDataADM.updateTextValueWithCommentRequest( + resourceIri = resourceIri, + valueIri = textValueWithoutStandoffIri.get, + valueAsString = valueAsString, + valueHasComment = "Updated comment" + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + textValueWithoutStandoffIri.set(valueIri) + val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) + valueType should ===(OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = textValueWithoutStandoffIri.get, + userEmail = anythingUserEmail + ) + + val savedValueAsString: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueAsString) + savedValueAsString should ===(valueAsString) + } + + "create a text value without standoff and with a comment" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val valueAsString: String = "this is a text value that has a comment" + val valueHasComment: String = "this is a comment" + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLDEntity = SharedTestDataADM.createTextValueWithCommentRequest( + resourceIri = resourceIri, + valueAsString = valueAsString, + valueHasComment = valueHasComment + ) + + val request = Post(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + textValueWithoutStandoffIri.set(valueIri) + val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) + valueType should ===(OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = textValueWithoutStandoffIri.get, + userEmail = anythingUserEmail + ) + + val savedValueAsString: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueAsString) + savedValueAsString should ===(valueAsString) + val savedValueHasComment: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasComment) + savedValueHasComment should ===(valueHasComment) + } "create a text value with standoff" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri @@ -1007,43 +1195,6 @@ class ValuesRouteV2E2ESpec extends E2ESpec { assert(savedTextValueAsXml.contains(textValueAsXml)) } - "create a text value with a comment" in { - val resourceIri: IRI = SharedTestDataADM.AThing.iri - val valueAsString: String = "this is a text value that has a comment" - val valueHasComment: String = "this is a comment" - val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri - val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) - - val jsonLDEntity = SharedTestDataADM.createTextValueWithCommentRequest( - resourceIri = resourceIri, - valueAsString = valueAsString, - valueHasComment = valueHasComment - ) - - val request = Post(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) - val response: HttpResponse = singleAwaitingRequest(request) - assert(response.status == StatusCodes.OK, response.toString) - val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) - val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) - textValueWithoutStandoffIri.set(valueIri) - val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) - valueType should ===(OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri) - - val savedValue: JsonLDObject = getValue( - resourceIri = resourceIri, - maybePreviousLastModDate = maybeResourceLastModDate, - propertyIriForGravsearch = propertyIri, - propertyIriInResult = propertyIri, - expectedValueIri = textValueWithoutStandoffIri.get, - userEmail = anythingUserEmail - ) - - val savedValueAsString: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueAsString) - savedValueAsString should ===(valueAsString) - val savedValueHasComment: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasComment) - savedValueHasComment should ===(valueHasComment) - } - "not create an empty text value" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri val valueAsString: String = "" @@ -1694,7 +1845,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { savedGeonameCode should ===(geonameCode) } - "create a link between two resources" in { + "create a link between two resources, without a comment" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri val linkPropertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing".toSmartIri val linkValuePropertyIri: SmartIri = linkPropertyIri.fromLinkPropToLinkValueProp @@ -1899,40 +2050,6 @@ class ValuesRouteV2E2ESpec extends E2ESpec { savedDecimalValue should ===(decimalValue) } - "update a text value without standoff" in { - val resourceIri: IRI = SharedTestDataADM.AThing.iri - val valueAsString: String = "Comment 1a updated" - val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasText".toSmartIri - val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) - - val jsonLDEntity = SharedTestDataADM.updateTextValueWithoutStandoffRequest( - resourceIri = resourceIri, - valueIri = textValueWithoutStandoffIri.get, - valueAsString = valueAsString - ) - - val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) - val response: HttpResponse = singleAwaitingRequest(request) - assert(response.status == StatusCodes.OK, response.toString) - val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) - val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) - textValueWithoutStandoffIri.set(valueIri) - val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) - valueType should ===(OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri) - - val savedValue: JsonLDObject = getValue( - resourceIri = resourceIri, - maybePreviousLastModDate = maybeResourceLastModDate, - propertyIriForGravsearch = propertyIri, - propertyIriInResult = propertyIri, - expectedValueIri = textValueWithoutStandoffIri.get, - userEmail = anythingUserEmail - ) - - val savedValueAsString: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueAsString) - savedValueAsString should ===(valueAsString) - } - "update a text value with standoff" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri @@ -2038,21 +2155,6 @@ class ValuesRouteV2E2ESpec extends E2ESpec { savedValueHasComment should ===(valueHasComment) } - "not update a text value so it's empty" in { - val resourceIri: IRI = SharedTestDataADM.AThing.iri - val valueAsString: String = "" - - val jsonLDEntity = SharedTestDataADM.updateTextValueWithoutStandoffRequest( - resourceIri = resourceIri, - valueIri = textValueWithoutStandoffIri.get, - valueAsString = valueAsString - ) - - val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) - val response: HttpResponse = singleAwaitingRequest(request) - assert(response.status == StatusCodes.BadRequest, response.toString) - } - "update a date value representing a range with day precision" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri val propertyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasDate".toSmartIri @@ -2701,6 +2803,158 @@ class ValuesRouteV2E2ESpec extends E2ESpec { savedTargetIri should ===(linkTargetIri) } + "not update a link without a comment without changing it" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val linkPropertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing".toSmartIri + val linkValuePropertyIri: SmartIri = linkPropertyIri.fromLinkPropToLinkValueProp + val linkTargetIri: IRI = "http://rdfh.ch/0001/5IEswyQFQp2bxXDrOyEfEA" + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLdEntity = SharedTestDataADM.updateLinkValueRequest( + resourceIri = resourceIri, + valueIri = linkValueIri.get, + targetResourceIri = linkTargetIri + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.BadRequest, response.toString) + } + + "update a link between two resources, adding a comment" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val linkPropertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing".toSmartIri + val linkValuePropertyIri: SmartIri = linkPropertyIri.fromLinkPropToLinkValueProp + val linkTargetIri: IRI = "http://rdfh.ch/0001/5IEswyQFQp2bxXDrOyEfEA" + val comment = "adding a comment" + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLdEntity = SharedTestDataADM.updateLinkValueRequest( + resourceIri = resourceIri, + valueIri = linkValueIri.get, + targetResourceIri = linkTargetIri, + comment = Some(comment) + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + linkValueIri.set(valueIri) + val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) + valueType should ===(OntologyConstants.KnoraApiV2Complex.LinkValue.toSmartIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = linkPropertyIri, + propertyIriInResult = linkValuePropertyIri, + expectedValueIri = linkValueIri.get, + userEmail = anythingUserEmail + ) + + val savedComment: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasComment) + savedComment should ===(comment) + } + + "update a link between two resources, changing only the comment" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val linkPropertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing".toSmartIri + val linkValuePropertyIri: SmartIri = linkPropertyIri.fromLinkPropToLinkValueProp + val linkTargetIri: IRI = "http://rdfh.ch/0001/5IEswyQFQp2bxXDrOyEfEA" + val comment = "changing only the comment" + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLdEntity = SharedTestDataADM.updateLinkValueRequest( + resourceIri = resourceIri, + valueIri = linkValueIri.get, + targetResourceIri = linkTargetIri, + comment = Some(comment) + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + linkValueIri.set(valueIri) + val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) + valueType should ===(OntologyConstants.KnoraApiV2Complex.LinkValue.toSmartIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = linkPropertyIri, + propertyIriInResult = linkValuePropertyIri, + expectedValueIri = linkValueIri.get, + userEmail = anythingUserEmail + ) + + val savedComment: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasComment) + savedComment should ===(comment) + } + + "not update a link with a comment without changing it" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val linkPropertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing".toSmartIri + val linkTargetIri: IRI = "http://rdfh.ch/0001/5IEswyQFQp2bxXDrOyEfEA" + val comment = "changing only the comment" + + val jsonLdEntity = SharedTestDataADM.updateLinkValueRequest( + resourceIri = resourceIri, + valueIri = linkValueIri.get, + targetResourceIri = linkTargetIri, + comment = Some(comment) + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.BadRequest, response.toString) + } + + "create a link between two resources, with a comment" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val linkPropertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThing".toSmartIri + val linkValuePropertyIri: SmartIri = linkPropertyIri.fromLinkPropToLinkValueProp + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + val comment = "Initial comment" + + val jsonLdEntity = SharedTestDataADM.createLinkValueRequest( + resourceIri = resourceIri, + targetResourceIri = SharedTestDataADM.TestDing.iri, + valueHasComment = Some(comment) + ) + + val request = Post(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + linkValueIri.set(valueIri) + val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) + valueType should ===(OntologyConstants.KnoraApiV2Complex.LinkValue.toSmartIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = linkPropertyIri, + propertyIriInResult = linkValuePropertyIri, + expectedValueIri = linkValueIri.get, + userEmail = anythingUserEmail + ) + + val savedTarget: JsonLDObject = savedValue.requireObject(OntologyConstants.KnoraApiV2Complex.LinkValueHasTarget) + val savedTargetIri: IRI = savedTarget.requireString(JsonLDConstants.ID) + savedTargetIri should ===(SharedTestDataADM.TestDing.iri) + + val savedComment: String = savedValue.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasComment) + savedComment should ===(comment) + } + "delete an integer value" in { val jsonLdEntity = SharedTestDataADM.deleteIntValueRequest( resourceIri = SharedTestDataADM.AThing.iri, diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index a4a57c4647..8ad5c0e948 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -403,14 +403,289 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { } } - "create an integer value with custom permissions" in { + "not create a duplicate integer value" in { + val resourceIri: IRI = aThingIri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val intValue = 4 + + responderManager ! CreateValueRequestV2( + CreateValueV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = propertyIri, + valueContent = IntegerValueContentV2( + ontologySchema = ApiV2Complex, + valueHasInteger = intValue + ) + ), + requestingUser = anythingUser1, + apiRequestID = UUID.randomUUID + ) + + expectMsgPF(timeout) { + case msg: akka.actor.Status.Failure => + msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + } + } + + "update an integer value" in { + val resourceIri: IRI = aThingIri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUser1) + + // Get the value before update. + val previousValueFromTriplestore: ReadValueV2 = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueIri.get, + requestingUser = anythingUser1, + checkLastModDateChanged = false + ) + + // Update the value. + + val intValue = 5 + + responderManager ! UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = propertyIri, + valueIri = intValueIri.get, + valueContent = IntegerValueContentV2( + ontologySchema = ApiV2Complex, + valueHasInteger = intValue + ) + ), + requestingUser = anythingUser1, + apiRequestID = UUID.randomUUID + ) + + expectMsgPF(timeout) { + case updateValueResponse: UpdateValueResponseV2 => intValueIri.set(updateValueResponse.valueIri) + } + + // Read the value back to check that it was added correctly. + + val updatedValueFromTriplestore = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueIri.get, + requestingUser = anythingUser1 + ) + + updatedValueFromTriplestore.valueContent match { + case savedValue: IntegerValueContentV2 => + savedValue.valueHasInteger should ===(intValue) + updatedValueFromTriplestore.permissions should ===(previousValueFromTriplestore.permissions) + updatedValueFromTriplestore.valueHasUUID should ===(previousValueFromTriplestore.valueHasUUID) + + case _ => throw AssertionException(s"Expected integer value, got $updatedValueFromTriplestore") + } + + // Check that the permissions and UUID were deleted from the previous version of the value. + assert(getValueUUID(previousValueFromTriplestore.valueIri).isEmpty) + assert(getValuePermissions(previousValueFromTriplestore.valueIri).isEmpty) + } + + "not update an integer value without a comment without changing it" in { + val resourceIri: IRI = aThingIri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val intValue = 5 + + responderManager ! UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = propertyIri, + valueIri = intValueIri.get, + valueContent = IntegerValueContentV2( + ontologySchema = ApiV2Complex, + valueHasInteger = intValue + ) + ), + requestingUser = anythingUser1, + apiRequestID = UUID.randomUUID + ) + + expectMsgPF(timeout) { + case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + } + } + + "update an integer value, adding a comment" in { + val resourceIri: IRI = aThingIri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUser1) + val comment = "Added a comment" + + // Get the value before update. + val previousValueFromTriplestore: ReadValueV2 = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueIri.get, + requestingUser = anythingUser1, + checkLastModDateChanged = false + ) + + // Update the value. + + val intValue = 5 + + responderManager ! UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = propertyIri, + valueIri = intValueIri.get, + valueContent = IntegerValueContentV2( + ontologySchema = ApiV2Complex, + valueHasInteger = intValue, + comment = Some(comment) + ) + ), + requestingUser = anythingUser1, + apiRequestID = UUID.randomUUID + ) + + expectMsgPF(timeout) { + case updateValueResponse: UpdateValueResponseV2 => intValueIri.set(updateValueResponse.valueIri) + } + + // Read the value back to check that it was added correctly. + + val updatedValueFromTriplestore = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueIri.get, + requestingUser = anythingUser1 + ) + + updatedValueFromTriplestore.valueContent match { + case savedValue: IntegerValueContentV2 => + savedValue.valueHasInteger should ===(intValue) + updatedValueFromTriplestore.permissions should ===(previousValueFromTriplestore.permissions) + updatedValueFromTriplestore.valueHasUUID should ===(previousValueFromTriplestore.valueHasUUID) + assert(updatedValueFromTriplestore.valueContent.comment.contains(comment)) + + case _ => throw AssertionException(s"Expected integer value, got $updatedValueFromTriplestore") + } + + // Check that the permissions and UUID were deleted from the previous version of the value. + assert(getValueUUID(previousValueFromTriplestore.valueIri).isEmpty) + assert(getValuePermissions(previousValueFromTriplestore.valueIri).isEmpty) + } + + "not update an integer value with a comment without changing it" in { + val resourceIri: IRI = aThingIri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val intValue = 5 + val comment = "Added a comment" + + responderManager ! UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = propertyIri, + valueIri = intValueIri.get, + valueContent = IntegerValueContentV2( + ontologySchema = ApiV2Complex, + valueHasInteger = intValue, + comment = Some(comment) + ) + ), + requestingUser = anythingUser1, + apiRequestID = UUID.randomUUID + ) + + expectMsgPF(timeout) { + case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + } + } + + "update an integer value with a comment, changing only the comment" in { + val resourceIri: IRI = aThingIri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUser1) + val comment = "An updated comment" + + // Get the value before update. + val previousValueFromTriplestore: ReadValueV2 = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueIri.get, + requestingUser = anythingUser1, + checkLastModDateChanged = false + ) + + // Update the value. + + val intValue = 5 + + responderManager ! UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = propertyIri, + valueIri = intValueIri.get, + valueContent = IntegerValueContentV2( + ontologySchema = ApiV2Complex, + valueHasInteger = intValue, + comment = Some(comment) + ) + ), + requestingUser = anythingUser1, + apiRequestID = UUID.randomUUID + ) + + expectMsgPF(timeout) { + case updateValueResponse: UpdateValueResponseV2 => intValueIri.set(updateValueResponse.valueIri) + } + + // Read the value back to check that it was added correctly. + + val updatedValueFromTriplestore = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueIri.get, + requestingUser = anythingUser1 + ) + + updatedValueFromTriplestore.valueContent match { + case savedValue: IntegerValueContentV2 => + savedValue.valueHasInteger should ===(intValue) + updatedValueFromTriplestore.permissions should ===(previousValueFromTriplestore.permissions) + updatedValueFromTriplestore.valueHasUUID should ===(previousValueFromTriplestore.valueHasUUID) + assert(updatedValueFromTriplestore.valueContent.comment.contains(comment)) + + case _ => throw AssertionException(s"Expected integer value, got $updatedValueFromTriplestore") + } + + // Check that the permissions and UUID were deleted from the previous version of the value. + assert(getValueUUID(previousValueFromTriplestore.valueIri).isEmpty) + assert(getValuePermissions(previousValueFromTriplestore.valueIri).isEmpty) + } + + "create an integer value with a comment" in { // Add the value. val resourceIri: IRI = aThingIri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri - val intValue = 1 - val permissions = "CR knora-admin:Creator|V http://rdfh.ch/groups/0001/thing-searcher" + val intValue = 8 val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUser1) + val comment = "Initial comment" responderManager ! CreateValueRequestV2( CreateValueV2( @@ -419,16 +694,17 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { propertyIri = propertyIri, valueContent = IntegerValueContentV2( ontologySchema = ApiV2Complex, - valueHasInteger = intValue - ), - permissions = Some(permissions) + valueHasInteger = intValue, + comment = Some(comment) + ) ), requestingUser = anythingUser1, apiRequestID = UUID.randomUUID ) expectMsgPF(timeout) { - case createValueResponse: CreateValueResponseV2 => intValueIriWithCustomPermissions.set(createValueResponse.valueIri) + case createValueResponse: CreateValueResponseV2 => + intValueIri.set(createValueResponse.valueIri) } // Read the value back to check that it was added correctly. @@ -438,24 +714,27 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = propertyIri, propertyIriInResult = propertyIri, - expectedValueIri = intValueIriWithCustomPermissions.get, + expectedValueIri = intValueIri.get, requestingUser = anythingUser1 ) valueFromTriplestore.valueContent match { case savedValue: IntegerValueContentV2 => savedValue.valueHasInteger should ===(intValue) - PermissionUtilADM.parsePermissions(valueFromTriplestore.permissions) should ===(PermissionUtilADM.parsePermissions(permissions)) + assert(savedValue.comment.contains(comment)) case _ => throw AssertionException(s"Expected integer value, got $valueFromTriplestore") } } - "not create an integer value with syntactically invalid custom permissions" in { + "create an integer value with custom permissions" in { + // Add the value. + val resourceIri: IRI = aThingIri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri - val intValue = 1024 - val permissions = "M knora-admin:Creator,V knora-admin:KnownUser" + val intValue = 1 + val permissions = "CR knora-admin:Creator|V http://rdfh.ch/groups/0001/thing-searcher" + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUser1) responderManager ! CreateValueRequestV2( CreateValueV2( @@ -473,16 +752,34 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) + case createValueResponse: CreateValueResponseV2 => intValueIriWithCustomPermissions.set(createValueResponse.valueIri) } + // Read the value back to check that it was added correctly. + + val valueFromTriplestore = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueIriWithCustomPermissions.get, + requestingUser = anythingUser1 + ) + + valueFromTriplestore.valueContent match { + case savedValue: IntegerValueContentV2 => + savedValue.valueHasInteger should ===(intValue) + PermissionUtilADM.parsePermissions(valueFromTriplestore.permissions) should ===(PermissionUtilADM.parsePermissions(permissions)) + + case _ => throw AssertionException(s"Expected integer value, got $valueFromTriplestore") + } } - "not create an integer value with custom permissions referring to a nonexistent group" in { + "not create an integer value with syntactically invalid custom permissions" in { val resourceIri: IRI = aThingIri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri val intValue = 1024 - val permissions = "M knora-admin:Creator|V http://rdfh.ch/groups/0001/nonexistent-group" + val permissions = "M knora-admin:Creator,V knora-admin:KnownUser" responderManager ! CreateValueRequestV2( CreateValueV2( @@ -500,14 +797,16 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) + case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[BadRequestException] should ===(true) } + } - "not create a value if the user does not have modify permission on the resource" in { + "not create an integer value with custom permissions referring to a nonexistent group" in { val resourceIri: IRI = aThingIri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri - val intValue = 5 + val intValue = 1024 + val permissions = "M knora-admin:Creator|V http://rdfh.ch/groups/0001/nonexistent-group" responderManager ! CreateValueRequestV2( CreateValueV2( @@ -517,21 +816,22 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { valueContent = IntegerValueContentV2( ontologySchema = ApiV2Complex, valueHasInteger = intValue - ) + ), + permissions = Some(permissions) ), - requestingUser = incunabulaUser, + requestingUser = anythingUser1, apiRequestID = UUID.randomUUID ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[ForbiddenException] should ===(true) + case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[NotFoundException] should ===(true) } } - "not create a duplicate integer value" in { + "not create a value if the user does not have modify permission on the resource" in { val resourceIri: IRI = aThingIri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri - val intValue = 4 + val intValue = 5 responderManager ! CreateValueRequestV2( CreateValueV2( @@ -543,13 +843,12 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { valueHasInteger = intValue ) ), - requestingUser = anythingUser1, + requestingUser = incunabulaUser, apiRequestID = UUID.randomUUID ) expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => - msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[ForbiddenException] should ===(true) } } @@ -1854,70 +2153,6 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { standoffLinkValueIri.set(linkValueFromTriplestore.valueIri) } - "update an integer value" in { - val resourceIri: IRI = aThingIri - val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri - val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUser1) - - // Get the value before update. - val previousValueFromTriplestore: ReadValueV2 = getValue( - resourceIri = resourceIri, - maybePreviousLastModDate = maybeResourceLastModDate, - propertyIriForGravsearch = propertyIri, - propertyIriInResult = propertyIri, - expectedValueIri = intValueIri.get, - requestingUser = anythingUser1, - checkLastModDateChanged = false - ) - - // Update the value. - - val intValue = 5 - - responderManager ! UpdateValueRequestV2( - UpdateValueContentV2( - resourceIri = resourceIri, - resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, - propertyIri = propertyIri, - valueIri = intValueIri.get, - valueContent = IntegerValueContentV2( - ontologySchema = ApiV2Complex, - valueHasInteger = intValue - ) - ), - requestingUser = anythingUser1, - apiRequestID = UUID.randomUUID - ) - - expectMsgPF(timeout) { - case updateValueResponse: UpdateValueResponseV2 => intValueIri.set(updateValueResponse.valueIri) - } - - // Read the value back to check that it was added correctly. - - val updatedValueFromTriplestore = getValue( - resourceIri = resourceIri, - maybePreviousLastModDate = maybeResourceLastModDate, - propertyIriForGravsearch = propertyIri, - propertyIriInResult = propertyIri, - expectedValueIri = intValueIri.get, - requestingUser = anythingUser1 - ) - - updatedValueFromTriplestore.valueContent match { - case savedValue: IntegerValueContentV2 => - savedValue.valueHasInteger should ===(intValue) - updatedValueFromTriplestore.permissions should ===(previousValueFromTriplestore.permissions) - updatedValueFromTriplestore.valueHasUUID should ===(previousValueFromTriplestore.valueHasUUID) - - case _ => throw AssertionException(s"Expected integer value, got $updatedValueFromTriplestore") - } - - // Check that the permissions and UUID were deleted from the previous version of the value. - assert(getValueUUID(previousValueFromTriplestore.valueIri).isEmpty) - assert(getValuePermissions(previousValueFromTriplestore.valueIri).isEmpty) - } - "not update a value if an outdated value IRI is given" in { val resourceIri: IRI = aThingIri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri @@ -2238,31 +2473,6 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { } } - "not update an integer value without changing it" in { - val resourceIri: IRI = aThingIri - val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri - val intValue = 6 - - responderManager ! UpdateValueRequestV2( - UpdateValueContentV2( - resourceIri = resourceIri, - resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, - propertyIri = propertyIri, - valueIri = intValueIri.get, - valueContent = IntegerValueContentV2( - ontologySchema = ApiV2Complex, - valueHasInteger = intValue - ) - ), - requestingUser = anythingUser1, - apiRequestID = UUID.randomUUID - ) - - expectMsgPF(timeout) { - case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) - } - } - "update a text value (without submitting standoff)" in { val valueHasString = "This updated comment has no standoff" val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book_comment".toSmartIri @@ -3354,7 +3564,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { } } - "not update a link without changing it" in { + "not update a link without a comment without changing it" in { val resourceIri: IRI = "http://rdfh.ch/0803/cb1a74e3e2f6" val linkValuePropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkToValue.toSmartIri @@ -3380,6 +3590,177 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { } } + "update a link, adding a comment" in { + val resourceIri: IRI = "http://rdfh.ch/0803/cb1a74e3e2f6" + val linkPropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkTo.toSmartIri + val linkValuePropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkToValue.toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, incunabulaUser) + val comment: String = "Adding a comment" + + val updateValueRequest = UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = OntologyConstants.KnoraApiV2Complex.LinkObj.toSmartIri, + propertyIri = linkValuePropertyIri, + valueIri = linkValueIri.get, + valueContent = LinkValueContentV2( + ontologySchema = ApiV2Complex, + referredResourceIri = generationeIri, + comment = Some(comment) + ) + ), + requestingUser = incunabulaUser, + apiRequestID = UUID.randomUUID + ) + + responderManager ! updateValueRequest + + expectMsgPF(timeout) { + case updateValueResponse: UpdateValueResponseV2 => linkValueIri.set(updateValueResponse.valueIri) + } + + val valueFromTriplestore = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = linkPropertyIri, + propertyIriInResult = linkValuePropertyIri, + expectedValueIri = linkValueIri.get, + requestingUser = incunabulaUser + ) + + valueFromTriplestore match { + case readLinkValueV2: ReadLinkValueV2 => + readLinkValueV2.valueContent.referredResourceIri should ===(generationeIri) + readLinkValueV2.valueHasRefCount should ===(1) + assert(readLinkValueV2.valueContent.comment.contains(comment)) + + case _ => throw AssertionException(s"Expected link value, got $valueFromTriplestore") + } + } + + "not update a link with a comment without changing it" in { + val resourceIri: IRI = "http://rdfh.ch/0803/cb1a74e3e2f6" + val linkValuePropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkToValue.toSmartIri + val comment: String = "Adding a comment" + + val updateValueRequest = UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = OntologyConstants.KnoraApiV2Complex.LinkObj.toSmartIri, + propertyIri = linkValuePropertyIri, + valueIri = linkValueIri.get, + valueContent = LinkValueContentV2( + ontologySchema = ApiV2Complex, + referredResourceIri = generationeIri, + comment = Some(comment) + ) + ), + requestingUser = incunabulaUser, + apiRequestID = UUID.randomUUID + ) + + responderManager ! updateValueRequest + + expectMsgPF(timeout) { + case msg: akka.actor.Status.Failure => msg.cause.isInstanceOf[DuplicateValueException] should ===(true) + } + } + + "update a link with a comment, changing only the comment" in { + val resourceIri: IRI = "http://rdfh.ch/0803/cb1a74e3e2f6" + val linkPropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkTo.toSmartIri + val linkValuePropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkToValue.toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, incunabulaUser) + val comment = "An updated comment" + + val updateValueRequest = UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = OntologyConstants.KnoraApiV2Complex.LinkObj.toSmartIri, + propertyIri = linkValuePropertyIri, + valueIri = linkValueIri.get, + valueContent = LinkValueContentV2( + ontologySchema = ApiV2Complex, + referredResourceIri = generationeIri, + comment = Some(comment) + ) + ), + requestingUser = incunabulaUser, + apiRequestID = UUID.randomUUID + ) + + responderManager ! updateValueRequest + + expectMsgPF(timeout) { + case updateValueResponse: UpdateValueResponseV2 => linkValueIri.set(updateValueResponse.valueIri) + } + + val valueFromTriplestore = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = linkPropertyIri, + propertyIriInResult = linkValuePropertyIri, + expectedValueIri = linkValueIri.get, + requestingUser = incunabulaUser + ) + + valueFromTriplestore match { + case readLinkValueV2: ReadLinkValueV2 => + readLinkValueV2.valueContent.referredResourceIri should ===(generationeIri) + readLinkValueV2.valueHasRefCount should ===(1) + assert(readLinkValueV2.valueContent.comment.contains(comment)) + + case _ => throw AssertionException(s"Expected link value, got $valueFromTriplestore") + } + } + + "create a link with a comment" in { + val resourceIri: IRI = "http://rdfh.ch/0803/cb1a74e3e2f6" + val linkPropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkTo.toSmartIri + val linkValuePropertyIri: SmartIri = OntologyConstants.KnoraApiV2Complex.HasLinkToValue.toSmartIri + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, incunabulaUser) + val comment = "Initial comment" + + val createValueRequest = CreateValueRequestV2( + CreateValueV2( + resourceIri = resourceIri, + propertyIri = linkValuePropertyIri, + resourceClassIri = OntologyConstants.KnoraApiV2Complex.LinkObj.toSmartIri, + valueContent = LinkValueContentV2( + ontologySchema = ApiV2Complex, + referredResourceIri = zeitglöckleinIri, + comment = Some(comment) + ) + ), + requestingUser = incunabulaUser, + apiRequestID = UUID.randomUUID + ) + + responderManager ! createValueRequest + + expectMsgPF(timeout) { + case createValueResponse: CreateValueResponseV2 => linkValueIri.set(createValueResponse.valueIri) + } + + val valueFromTriplestore = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = linkPropertyIri, + propertyIriInResult = linkValuePropertyIri, + expectedValueIri = linkValueIri.get, + requestingUser = incunabulaUser + ) + + valueFromTriplestore match { + case readLinkValueV2: ReadLinkValueV2 => + readLinkValueV2.valueContent.referredResourceIri should ===(zeitglöckleinIri) + readLinkValueV2.valueHasRefCount should ===(1) + assert(readLinkValueV2.valueContent.comment.contains(comment)) + + case _ => throw AssertionException(s"Expected link value, got $valueFromTriplestore") + } + } + "not update a standoff link directly" in { responderManager ! UpdateValueRequestV2( UpdateValueContentV2( @@ -3694,7 +4075,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = linkPropertyIri, propertyIriInResult = linkValuePropertyIri, - valueIri = intValueIri.get, + valueIri = linkValueIri.get, requestingUser = anythingUser1) }