From 4d8f8670f30c81581a0d1edd0758a15f50010df7 Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Tue, 8 Sep 2020 12:27:57 +0200 Subject: [PATCH] feat(api-v2): Accept custom new value IRI when updating value (#1698) --- docs/03-apis/api-v2/editing-resources.md | 16 +- docs/03-apis/api-v2/editing-values.md | 30 +++- .../webapi/messages/OntologyConstants.scala | 1 + .../webapi/messages/StringFormatter.scala | 24 +++ .../resourcemessages/ResourceMessagesV2.scala | 24 ++- .../valuemessages/ValueMessagesV2.scala | 99 ++++++++---- .../responders/v2/ValuesResponderV2.scala | 68 +++++---- .../sharedtestdata/SharedTestDataADM.scala | 80 ++++++---- .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 20 ++- .../webapi/e2e/v2/ValuesRouteV2E2ESpec.scala | 143 +++++++++++++++--- .../responders/v2/ValuesResponderV2Spec.scala | 76 +++++++--- 11 files changed, 446 insertions(+), 135 deletions(-) diff --git a/docs/03-apis/api-v2/editing-resources.md b/docs/03-apis/api-v2/editing-resources.md index 20e35a38a8..6e4337e06d 100644 --- a/docs/03-apis/api-v2/editing-resources.md +++ b/docs/03-apis/api-v2/editing-resources.md @@ -184,13 +184,23 @@ project or a system administrator. The specified creator must also have permission to create resources of that class in that project. In addition to the creation date, in the body of the request, it is possible to specify a custom IRI (of [Knora IRI](knora-iris.md#iris-for-data) form) for a resource through -the `@id` attribute which will then be assigned to the resource; otherwise the resource will get a unique random IRI. +the `@id` attribute which will then be assigned to the resource; otherwise the resource will get a unique random IRI. + +A custom resource IRI must be `http://rdfh.ch/PROJECT_SHORTCODE/` (where `PROJECT_SHORTCODE` +is the shortcode of the project that the resource belongs to), plus a custom ID string. + Similarly, it is possible to assign a custom IRI to the values using their `@id` attributes; if not given, random IRIs -will be assigned to the values. An optional custom UUID of a value can also be given by adding `knora-api:valueHasUUID`. +will be assigned to the values. + +A custom value IRI must be the IRI of the containing resource, followed +by a `/values/` and a custom ID string. + +An optional custom UUID of a value can also be given by adding `knora-api:valueHasUUID`. Each custom UUID must be [base64url-encoded](https://tools.ietf.org/html/rfc4648#section-5), without padding. Each value of the new resource can also have a custom creation date specified by adding `knora-api:creationDate` (an [xsd:dateTimeStamp](https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp)). For example: + ```jsonld { "@id" : "http://rdfh.ch/0001/a-custom-thing", @@ -199,7 +209,7 @@ For example: "@id" : "http://rdfh.ch/projects/0001" }, "anything:hasInteger" : { - "@id" : "http://rdfh.ch/0001/a-thing/values/int-value-IRI", + "@id" : "http://rdfh.ch/0001/a-custom-thing/values/int-value-IRI", "@type" : "knora-api:IntValue", "knora-api:intValueAsInt" : 10, "knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ", diff --git a/docs/03-apis/api-v2/editing-values.md b/docs/03-apis/api-v2/editing-values.md index e7710cca48..356c8b2445 100644 --- a/docs/03-apis/api-v2/editing-values.md +++ b/docs/03-apis/api-v2/editing-values.md @@ -87,14 +87,15 @@ Permissions for the new value can be given by adding `knora-api:hasPermissions`. Each value can have an optional custom IRI (of [Knora IRI](knora-iris.md#iris-for-data) form) specified by the `@id` attribute, a custom creation date specified by adding `knora-api:valueCreationDate` (an [xsd:dateTimeStamp](https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp)), or a custom UUID given by `knora-api:valueHasUUID`. Each custom UUID must be [base64url-encoded](rfc:4648#section-5), without padding. -For example: +A custom value IRI must be the IRI of the containing resource, followed +by a `/values/` and a custom ID string. For example: ```jsonld "@id" : "http://rdfh.ch/0001/a-thing", "@type" : "anything:Thing", "anything:hasInteger" : { - "@id" : "http://rdfh.ch/0001/a-customized-thing/values/int-value-IRI", + "@id" : "http://rdfh.ch/0001/a-thing/values/int-value-IRI", "@type" : "knora-api:IntValue", "knora-api:intValueAsInt" : 21, "knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ", @@ -391,6 +392,31 @@ well as on the value. To update a value and give it a custom timestamp, add `knora-api:valueCreationDate` (an [xsd:dateTimeStamp](https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp)). +To update a value and give the new version a custom IRI, add +`knora-api:newValueVersionIri`, like this: + +```jsonld +{ + "@id" : "http://rdfh.ch/0001/a-thing", + "@type" : "anything:Thing", + "anything:hasInteger" : { + "@id" : "http://rdfh.ch/0001/a-thing/values/vp96riPIRnmQcbMhgpv_Rg", + "@type" : "knora-api:IntValue", + "knora-api:intValueAsInt" : 21, + "knora-api:newValueVersionIri" : { + "@id" : "http://rdfh.ch/0001/a-thing/values/int-value-IRI" + } + }, + "@context" : { + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } +} +``` + +A custom value IRI must be the IRI of the containing resource, followed +by a `/values/` and a custom ID string. + The response is a JSON-LD document containing only `@id` and `@type`, returning the IRI and type of the new value version. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala index 3cd8ef4942..061ca8681d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala @@ -733,6 +733,7 @@ object OntologyConstants { val ValueCreationDate: IRI = KnoraApiV2PrefixExpansion + "valueCreationDate" val ValueHasUUID: IRI = KnoraApiV2PrefixExpansion + "valueHasUUID" val ValueHasComment: IRI = KnoraApiV2PrefixExpansion + "valueHasComment" + val NewValueVersionIri: IRI = KnoraApiV2PrefixExpansion + "newValueVersionIri" val User: IRI = KnoraApiV2PrefixExpansion + "User" val AttachedToUser: IRI = KnoraApiV2PrefixExpansion + "attachedToUser" diff --git a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala index 5a2c67ef24..ee17028e72 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -3160,4 +3160,28 @@ class StringFormatter private(val maybeSettings: Option[KnoraSettingsImpl] = Non "$1" + separator + "$2" ).toLowerCase } + + /** + * Validates a custom value IRI, throwing [[BadRequestException]] if the IRI is not valid. + * + * @param customValueIri the custom value IRI to be validated. + * @param projectCode the project code of the containing resource. + * @param resourceID the ID of the containing resource. + * @return the validated IRI. + */ + def validateCustomValueIri(customValueIri: SmartIri, projectCode: String, resourceID: String): SmartIri = { + if (!customValueIri.isKnoraValueIri) { + throw BadRequestException(s"<$customValueIri> is not a Knora value IRI") + } + + if (!customValueIri.getProjectCode.contains(projectCode)) { + throw BadRequestException(s"The provided value IRI does not contain the correct project code") + } + + if (!customValueIri.getResourceID.contains(resourceID)) { + throw BadRequestException(s"The provided value IRI does not contain the correct resource ID") + } + + customValueIri + } } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index fbfd77ecbd..173193e5ce 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -542,9 +542,25 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource // Get the resource's rdfs:label. label: String = jsonLDDocument.requireStringWithValidation(OntologyConstants.Rdfs.Label, stringFormatter.toSparqlEncodedString) - // Get the resource's project. + // Get information about the project that the resource should be created in. projectIri: SmartIri = jsonLDDocument.requireIriInObject(OntologyConstants.KnoraApiV2Complex.AttachedToProject, stringFormatter.toSmartIriWithErr) + projectInfoResponse: ProjectGetResponseADM <- (responderManager ? ProjectGetRequestADM( + ProjectIdentifierADM(maybeIri = Some(projectIri.toString)), + requestingUser = requestingUser + )).mapTo[ProjectGetResponseADM] + + _ = maybeCustomResourceIri.foreach { + definedResourceIri => + if (!definedResourceIri.isKnoraResourceIri) { + throw BadRequestException(s"<$definedResourceIri> is not a Knora resource IRI") + } + + if (!definedResourceIri.getProjectCode.contains(projectInfoResponse.project.shortcode)) { + throw BadRequestException(s"The provided resource IRI does not contain the correct project code") + } + } + // Get the resource's permissions. permissions: Option[String] = jsonLDDocument.maybeStringWithValidation(OntologyConstants.KnoraApiV2Complex.HasPermissions, stringFormatter.toSparqlEncodedString) @@ -635,12 +651,6 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource }.toMap propertyValuesMap: Map[SmartIri, Seq[CreateValueInNewResourceV2]] <- ActorUtil.sequenceSeqFuturesInMap(propertyValueFuturesMap) - - // Get information about the project that the resource should be created in. - projectInfoResponse: ProjectGetResponseADM <- (responderManager ? ProjectGetRequestADM( - ProjectIdentifierADM(maybeIri = Some(projectIri.toString)), - requestingUser = requestingUser - )).mapTo[ProjectGetResponseADM] } yield CreateResourceRequestV2( createResource = CreateResourceV2( resourceIri = maybeCustomResourceIri, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index c9d783ff7d..2149106efc 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -116,8 +116,15 @@ object CreateValueRequestV2 extends KnoraJsonLDRequestReaderV2[CreateValueReques log = log ) - // Get the custom value IRI if provided. - maybeCustomValueIri: Option[SmartIri] = jsonLDObject.maybeIDAsKnoraDataIri + // Get and validate the custom value IRI if provided. + maybeCustomValueIri: Option[SmartIri] = jsonLDObject.maybeIDAsKnoraDataIri.map { + definedNewIri => + stringFormatter.validateCustomValueIri( + customValueIri = definedNewIri, + projectCode = resourceIri.getProjectCode.get, + resourceID = resourceIri.getResourceID.get + ) + } // Get the custom value UUID if provided. maybeCustomUUID: Option[UUID] = jsonLDObject.maybeUUID(OntologyConstants.KnoraApiV2Complex.ValueHasUUID) @@ -248,7 +255,7 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques // Get the resource property and the new value version. updateValue: UpdateValueV2 <- jsonLDDocument.requireResourcePropertyValue match { case (propertyIri: SmartIri, jsonLDObject: JsonLDObject) => - val valueIri: IRI = jsonLDObject.requireIDAsKnoraDataIri.toString + val valueIri: SmartIri = jsonLDObject.requireIDAsKnoraDataIri // Get the custom value creation date, if provided. val maybeValueCreationDate: Option[Instant] = jsonLDObject.maybeDatatypeValueInObject( @@ -257,11 +264,35 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques validationFun = stringFormatter.xsdDateTimeStampToInstant ) - // Does the value object just contain knora-api:hasPermissions? + // Get and validate the custom new value version IRI, if provided. + + val maybeNewIri: Option[SmartIri] = jsonLDObject.maybeIriInObject( + OntologyConstants.KnoraApiV2Complex.NewValueVersionIri, + stringFormatter.toSmartIriWithErr + ).map { + definedNewIri => + if (definedNewIri == valueIri) { + throw BadRequestException(s"The IRI of a new value version cannot be the same as the IRI of the current version") + } + + stringFormatter.validateCustomValueIri( + customValueIri = definedNewIri, + projectCode = valueIri.getProjectCode.get, + resourceID = valueIri.getResourceID.get + ) + } - val valuePredicatesMinusIDAndType: Set[IRI] = jsonLDObject.value.keySet - JsonLDConstants.ID - JsonLDConstants.TYPE + // Aside from the value's ID and type and the optional predicates above, does the value object just + // contain knora-api:hasPermissions? - if (valuePredicatesMinusIDAndType == Set(OntologyConstants.KnoraApiV2Complex.HasPermissions)) { + val otherValuePredicates: Set[IRI] = jsonLDObject.value.keySet -- Set( + JsonLDConstants.ID, + JsonLDConstants.TYPE, + OntologyConstants.KnoraApiV2Complex.ValueCreationDate, + OntologyConstants.KnoraApiV2Complex.NewValueVersionIri + ) + + if (otherValuePredicates == Set(OntologyConstants.KnoraApiV2Complex.HasPermissions)) { // Yes. This is a request to change the value's permissions. val valueType: SmartIri = jsonLDObject.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) @@ -272,10 +303,11 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques resourceIri = resourceIri.toString, resourceClassIri = resourceClassIri, propertyIri = propertyIri, - valueIri = valueIri, + valueIri = valueIri.toString, valueType = valueType, permissions = permissions, - valueCreationDate = maybeValueCreationDate + valueCreationDate = maybeValueCreationDate, + newValueVersionIri = maybeNewIri ) ) } else { @@ -296,10 +328,11 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques resourceIri = resourceIri.toString, resourceClassIri = resourceClassIri, propertyIri = propertyIri, - valueIri = valueIri, + valueIri = valueIri.toString, valueContent = valueContent, permissions = maybePermissions, - valueCreationDate = maybeValueCreationDate + valueCreationDate = maybeValueCreationDate, + newValueVersionIri = maybeNewIri ) } } @@ -862,15 +895,17 @@ trait UpdateValueV2 { /** * A new version of a value of a Knora property to be created. * - * @param resourceIri the resource that the current value version is attached to. - * @param resourceClassIri the resource class that the client believes the resource belongs to. - * @param propertyIri the property that the client believes points to the value. If the value is a link value, - * this must be a link value property. - * @param valueIri the IRI of the value to be updated. - * @param valueContent the content of the new version of the value. - * @param permissions the permissions to be attached to the new value version. - * @param valueCreationDate an optional custom creation date to be attached to the new value version. If not supplied, - * the current time will be used. + * @param resourceIri the resource that the current value version is attached to. + * @param resourceClassIri the resource class that the client believes the resource belongs to. + * @param propertyIri the property that the client believes points to the value. If the value is a link value, + * this must be a link value property. + * @param valueIri the IRI of the value to be updated. + * @param valueContent the content of the new version of the value. + * @param permissions the permissions to be attached to the new value version. + * @param valueCreationDate an optional custom creation date to be attached to the new value version. If not provided, + * the current time will be used. + * @param newValueVersionIri an optional IRI to be used for the new value version. If not provided, a random IRI + * will be generated. */ case class UpdateValueContentV2(resourceIri: IRI, resourceClassIri: SmartIri, @@ -878,20 +913,23 @@ case class UpdateValueContentV2(resourceIri: IRI, valueIri: IRI, valueContent: ValueContentV2, permissions: Option[String] = None, - valueCreationDate: Option[Instant] = None) extends IOValueV2 with UpdateValueV2 + valueCreationDate: Option[Instant] = None, + newValueVersionIri: Option[SmartIri] = None) extends IOValueV2 with UpdateValueV2 /** * New permissions for a value. * - * @param resourceIri the resource that the current value version is attached to. - * @param resourceClassIri the resource class that the client believes the resource belongs to. - * @param propertyIri the property that the client believes points to the value. If the value is a link value, - * this must be a link value property. - * @param valueIri the IRI of the value to be updated. - * @param valueType the IRI of the value type. - * @param permissions the permissions to be attached to the new value version. - * @param valueCreationDate an optional custom creation date to be attached to the new value version. If not supplied, - * the current time will be used. + * @param resourceIri the resource that the current value version is attached to. + * @param resourceClassIri the resource class that the client believes the resource belongs to. + * @param propertyIri the property that the client believes points to the value. If the value is a link value, + * this must be a link value property. + * @param valueIri the IRI of the value to be updated. + * @param valueType the IRI of the value type. + * @param permissions the permissions to be attached to the new value version. + * @param valueCreationDate an optional custom creation date to be attached to the new value version. If not provided, + * the current time will be used. + * @param newValueVersionIri an optional IRI to be used for the new value version. If not provided, a random IRI + * will be generated. */ case class UpdateValuePermissionsV2(resourceIri: IRI, resourceClassIri: SmartIri, @@ -899,7 +937,8 @@ case class UpdateValuePermissionsV2(resourceIri: IRI, valueIri: IRI, valueType: SmartIri, permissions: String, - valueCreationDate: Option[Instant] = None) extends UpdateValueV2 + valueCreationDate: Option[Instant] = None, + newValueVersionIri: Option[SmartIri] = None) extends UpdateValueV2 /** * The IRI and content of a new value or value version whose existence in the triplestore needs to be verified. 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 5781705782..63b95943f3 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 @@ -910,7 +910,8 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Do the update. dataNamedGraph: IRI = stringFormatter.projectDataNamedGraphV2(resourceInfo.projectADM) - newValueIri: IRI = stringFormatter.makeRandomValueIri(resourceInfo.resourceIri) + newValueIri: IRI <- checkOrCreateEntityIri(updateValuePermissionsV2.newValueVersionIri, stringFormatter.makeRandomValueIri(resourceInfo.resourceIri)) + currentTime: Instant = updateValuePermissionsV2.valueCreationDate.getOrElse(Instant.now) sparqlUpdate = org.knora.webapi.messages.twirl.queries.sparql.v2.txt.changeValuePermissions( @@ -1074,6 +1075,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde valueCreator = updateValueRequest.requestingUser.id, valuePermissions = newValueVersionPermissionLiteral, valueCreationDate = updateValueContentV2.valueCreationDate, + newValueVersionIri = updateValueContentV2.newValueVersionIri, requestingUser = updateValueRequest.requestingUser ) @@ -1087,6 +1089,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde valueCreator = updateValueRequest.requestingUser.id, valuePermissions = newValueVersionPermissionLiteral, valueCreationDate = updateValueContentV2.valueCreationDate, + newValueVersionIri = updateValueContentV2.newValueVersionIri, requestingUser = updateValueRequest.requestingUser ) } @@ -1142,15 +1145,16 @@ 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 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 valueCreationDate a custom value creation date. - * @param requestingUser the user making the request. + * @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 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 valueCreationDate a custom value creation date. + * @param newValueVersionIri an optional IRI to be used for the new value version. + * @param requestingUser the user making the request. * @return an [[UnverifiedValueV2]]. */ private def updateOrdinaryValueV2AfterChecks(dataNamedGraph: IRI, @@ -1161,9 +1165,10 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde valueCreator: IRI, valuePermissions: String, valueCreationDate: Option[Instant], + newValueVersionIri: Option[SmartIri], requestingUser: UserADM): Future[UnverifiedValueV2] = { for { - newValueIri: IRI <- makeUnusedValueIri(resourceInfo.resourceIri) + newValueIri: IRI <- checkOrCreateEntityIri(newValueVersionIri, stringFormatter.makeRandomValueIri(resourceInfo.resourceIri)) // If we're updating a text value, update direct links and LinkValues for any resource references in Standoff. standoffLinkUpdates: Seq[SparqlTemplateLinkUpdate] <- (currentValue.valueContent, newValueVersion) match { @@ -1255,15 +1260,16 @@ 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 [[ReadLinkValueV2]] 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 valueCreationDate a custom value creation date. - * @param requestingUser the user making the request. + * @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 [[ReadLinkValueV2]] 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 valueCreationDate a custom value creation date. + * @param newValueVersionIri an optional IRI to be used for the new value version. + * @param requestingUser the user making the request. * @return an [[UnverifiedValueV2]]. */ private def updateLinkValueV2AfterChecks(dataNamedGraph: IRI, @@ -1274,6 +1280,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde valueCreator: IRI, valuePermissions: String, valueCreationDate: Option[Instant], + newValueVersionIri: Option[SmartIri], requestingUser: UserADM): Future[UnverifiedValueV2] = { // Are we changing the link target? @@ -1294,6 +1301,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde sourceResourceInfo = resourceInfo, linkPropertyIri = linkPropertyIri, targetResourceIri = newLinkValue.referredResourceIri, + customNewLinkValueIri = newValueVersionIri, valueCreator = valueCreator, valuePermissions = valuePermissions, requestingUser = requestingUser @@ -1342,6 +1350,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde sourceResourceInfo = resourceInfo, linkPropertyIri = linkPropertyIri, targetResourceIri = currentLinkValue.valueContent.referredResourceIri, + customNewLinkValueIri = newValueVersionIri, valueCreator = valueCreator, valuePermissions = valuePermissions, requestingUser = requestingUser @@ -2165,17 +2174,19 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde * 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. + * @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 customNewLinkValueIri the optional custom IRI supplied for the link value. + * @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, + customNewLinkValueIri: Option[SmartIri] = None, valueCreator: IRI, valuePermissions: String, requestingUser: UserADM): Future[SparqlTemplateLinkUpdate] = { @@ -2193,8 +2204,9 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Yes. Make a SparqlTemplateLinkUpdate. for { - // Generate an IRI for the new LinkValue. - newLinkValueIri: IRI <- makeUnusedValueIri(sourceResourceInfo.resourceIri) + // If no custom IRI was provided, generate an IRI for the new LinkValue. + newLinkValueIri: IRI <- checkOrCreateEntityIri(customNewLinkValueIri, stringFormatter.makeRandomValueIri(sourceResourceInfo.resourceIri)) + } yield SparqlTemplateLinkUpdate( linkPropertyIri = linkPropertyIri, directLinkExists = true, diff --git a/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala b/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala index 8198e7d59f..86eb930dc5 100644 --- a/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala @@ -1303,6 +1303,30 @@ object SharedTestDataADM { |}""".stripMargin } + + def updateIntValueWithCustomNewValueVersionIriRequest(resourceIri: IRI, + valueIri: IRI, + intValue: Int, + newValueVersionIri: IRI): String = { + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasInteger" : { + | "@id" : "$valueIri", + | "@type" : "knora-api:IntValue", + | "knora-api:newValueVersionIri" : { + | "@id" : "$newValueVersionIri" + | }, + | "knora-api:intValueAsInt" : $intValue + | }, + | "@context" : { + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#" + | } + |}""".stripMargin + } + def updateIntValueWithCustomPermissionsRequest(resourceIri: IRI, valueIri: IRI, intValue: Int, @@ -2897,34 +2921,34 @@ object SharedTestDataADM { def addCommentToPropertyThatHasNoComment(anythingOntologyIri: IRI, anythingLastModDate: Instant): String = { s"""{ - | "@id": "$anythingOntologyIri", - | "@type": "owl:Ontology", - | "knora-api:lastModificationDate": { - | "@type": "xsd:dateTimeStamp", - | "@value": "$anythingLastModDate" - | }, - | "@graph": [ - | { - | "@id": "anything:hasBlueThing", - | "@type": "owl:ObjectProperty", - | "rdfs:comment": [ - | { - | "@language": "en", - | "@value": "asdas asd as dasdasdas" - | } - | ] - | } - | ], - | "@context": { - | "anything": "http://0.0.0.0:3333/ontology/0001/anything/v2#", - | "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - | "owl": "http://www.w3.org/2002/07/owl#", - | "xsd": "http://www.w3.org/2001/XMLSchema#", - | "knora-api": "http://api.knora.org/ontology/knora-api/v2#", - | "salsah-gui": "http://api.knora.org/ontology/salsah-gui/v2#" - | } - |}""".stripMargin + | "@id": "$anythingOntologyIri", + | "@type": "owl:Ontology", + | "knora-api:lastModificationDate": { + | "@type": "xsd:dateTimeStamp", + | "@value": "$anythingLastModDate" + | }, + | "@graph": [ + | { + | "@id": "anything:hasBlueThing", + | "@type": "owl:ObjectProperty", + | "rdfs:comment": [ + | { + | "@language": "en", + | "@value": "asdas asd as dasdasdas" + | } + | ] + | } + | ], + | "@context": { + | "anything": "http://0.0.0.0:3333/ontology/0001/anything/v2#", + | "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + | "owl": "http://www.w3.org/2002/07/owl#", + | "xsd": "http://www.w3.org/2001/XMLSchema#", + | "knora-api": "http://api.knora.org/ontology/knora-api/v2#", + | "salsah-gui": "http://api.knora.org/ontology/salsah-gui/v2#" + | } + |}""".stripMargin } object AThing { diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 974df8acc3..7a868e88cb 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -607,7 +607,7 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { assert(savedCreationDate == creationDate) } - "create a resource with a custom Iri" in { + "create a resource with a custom IRI" in { val customIRI: IRI = SharedTestDataADM.customResourceIRI val jsonLDEntity = SharedTestDataADM.createResourceWithCustomIRI(customIRI) val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) @@ -618,6 +618,24 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { assert(resourceIri == customIRI) } + "not create a resource with an invalid custom IRI" in { + val customIRI: IRI = "http://rdfh.ch/invalid-resource-IRI" + val jsonLDEntity = SharedTestDataADM.createResourceWithCustomIRI(customIRI) + val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + println(responseToString(response)) + assert(response.status == StatusCodes.BadRequest, response.toString) + } + + "not create a resource with a custom IRI containing the wrong project code" in { + val customIRI: IRI = "http://rdfh.ch/0803/a-thing-with-IRI" + val jsonLDEntity = SharedTestDataADM.createResourceWithCustomIRI(customIRI) + val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + println(responseToString(response)) + assert(response.status == StatusCodes.BadRequest, response.toString) + } + "return a DuplicateValueException during resource creation when the supplied resource Iri is not unique" in { // duplicate resource IRI 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 5429fba7ef..3fbb37f997 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 @@ -54,7 +54,8 @@ class ValuesRouteV2E2ESpec extends E2ESpec { private val password = "test" private val intValueIri = new MutableTestIri - private val intValueIriWithCustomCreationDate = new MutableTestIri + private val intValueWithCustomPermissionsIri = new MutableTestIri + private val intValueForRsyncIri = new MutableTestIri private val textValueWithoutStandoffIri = new MutableTestIri private val textValueWithStandoffIri = new MutableTestIri private val textValueWithEscapeIri = new MutableTestIri @@ -240,6 +241,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { intValueIri.set(valueIri) val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) valueType should ===(OntologyConstants.KnoraApiV2Complex.IntValue.toSmartIri) + integerValueUUID = responseJsonDoc.body.requireStringWithValidation(OntologyConstants.KnoraApiV2Complex.ValueHasUUID, stringFormatter.validateBase64EncodedUuid) val savedValue: JsonLDObject = getValue( resourceIri = resourceIri, @@ -257,7 +259,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { "create an integer value with a custom value IRI" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri val intValue: Int = 30 - val customValueIri: IRI = "http://rdfh.ch/0001/a-customized-thing/values/int-with-valueIRI" + val customValueIri: IRI = "http://rdfh.ch/0001/a-thing/values/int-with-valueIRI" val jsonLdEntity = SharedTestDataADM.createIntValueWithCustomValueIriRequest( resourceIri = resourceIri, @@ -281,7 +283,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { | "@id" : "${SharedTestDataADM.AThing.iri}", | "@type" : "anything:Thing", | "anything:hasInteger" : { - | "@id" : "http://rdfh.ch/0001/a-customized-thing/values/int-with-valueIRI", + | "@id" : "http://rdfh.ch/0001/a-thing/values/int-with-valueIRI", | "@type" : "knora-api:IntValue", | "knora-api:intValueAsInt" : 43 | }, @@ -297,7 +299,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { assert(response.status == StatusCodes.BadRequest, response.toString) val errorMessage: String = Await.result(Unmarshal(response.entity).to[String], 1.second) - val invalidIri: Boolean = errorMessage.contains(s"IRI: 'http://rdfh.ch/0001/a-customized-thing/values/int-with-valueIRI' already exists, try another one.") + val invalidIri: Boolean = errorMessage.contains(s"IRI: 'http://rdfh.ch/0001/a-thing/values/int-with-valueIRI' already exists, try another one.") invalidIri should be(true) } @@ -332,7 +334,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) - intValueIriWithCustomCreationDate.set(valueIri) + intValueForRsyncIri.set(valueIri) val savedCreationDate: Instant = responseJsonDoc.body.requireDatatypeValueInObject( key = OntologyConstants.KnoraApiV2Complex.ValueCreationDate, @@ -415,17 +417,16 @@ class ValuesRouteV2E2ESpec extends E2ESpec { assert(response.status == StatusCodes.OK, response.toString) val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) - intValueIri.set(valueIri) + intValueWithCustomPermissionsIri.set(valueIri) val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) valueType should ===(OntologyConstants.KnoraApiV2Complex.IntValue.toSmartIri) - integerValueUUID = responseJsonDoc.body.requireStringWithValidation(OntologyConstants.KnoraApiV2Complex.ValueHasUUID, stringFormatter.validateBase64EncodedUuid) val savedValue: JsonLDObject = getValue( resourceIri = resourceIri, maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = propertyIri, propertyIriInResult = propertyIri, - expectedValueIri = intValueIri.get, + expectedValueIri = intValueWithCustomPermissionsIri.get, userEmail = anythingUserEmail ) @@ -2072,7 +2073,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val jsonLdEntity = SharedTestDataADM.updateIntValueWithCustomCreationDateRequest( resourceIri = resourceIri, - valueIri = intValueIri.get, + valueIri = intValueForRsyncIri.get, intValue = intValue, valueCreationDate = valueCreationDate ) @@ -2082,18 +2083,16 @@ class ValuesRouteV2E2ESpec extends E2ESpec { assert(response.status == StatusCodes.OK, response.toString) val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) - intValueIri.set(valueIri) + intValueForRsyncIri.set(valueIri) val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) valueType should ===(OntologyConstants.KnoraApiV2Complex.IntValue.toSmartIri) - val newIntegerValueUUID: UUID = responseJsonDoc.body.requireStringWithValidation(OntologyConstants.KnoraApiV2Complex.ValueHasUUID, stringFormatter.validateBase64EncodedUuid) - assert(newIntegerValueUUID == integerValueUUID) // The new version should have the same UUID. val savedValue: JsonLDObject = getValue( resourceIri = resourceIri, maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = propertyIri, propertyIriInResult = propertyIri, - expectedValueIri = intValueIri.get, + expectedValueIri = intValueForRsyncIri.get, userEmail = anythingUserEmail ) @@ -2109,6 +2108,114 @@ class ValuesRouteV2E2ESpec extends E2ESpec { savedCreationDate should ===(valueCreationDate) } + "update an integer value with a custom new value version IRI" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val intValue: Int = 7 + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + val newValueVersionIri: IRI = s"http://rdfh.ch/0001/a-thing/values/updated-int-value" + + val jsonLdEntity = SharedTestDataADM.updateIntValueWithCustomNewValueVersionIriRequest( + resourceIri = resourceIri, + valueIri = intValueForRsyncIri.get, + intValue = intValue, + newValueVersionIri = newValueVersionIri + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + println(responseToString(response)) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + assert(valueIri == newValueVersionIri) + intValueForRsyncIri.set(valueIri) + + val savedValue: JsonLDObject = getValue( + resourceIri = resourceIri, + maybePreviousLastModDate = maybeResourceLastModDate, + propertyIriForGravsearch = propertyIri, + propertyIriInResult = propertyIri, + expectedValueIri = intValueForRsyncIri.get, + userEmail = anythingUserEmail + ) + + val intValueAsInt: Int = savedValue.requireInt(OntologyConstants.KnoraApiV2Complex.IntValueAsInt) + intValueAsInt should ===(intValue) + } + + "not update an integer value with a custom new value version IRI that is the same as the current IRI" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val intValue: Int = 8 + val newValueVersionIri: IRI = s"http://rdfh.ch/0001/a-thing/values/updated-int-value" + + val jsonLdEntity = SharedTestDataADM.updateIntValueWithCustomNewValueVersionIriRequest( + resourceIri = resourceIri, + valueIri = intValueForRsyncIri.get, + intValue = intValue, + newValueVersionIri = newValueVersionIri + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + val responseAsString = responseToString(response) + assert(response.status == StatusCodes.BadRequest, responseAsString) + } + + "not update an integer value with an invalid custom new value version IRI" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val intValue: Int = 8 + val newValueVersionIri: IRI = "foo" + + val jsonLdEntity = SharedTestDataADM.updateIntValueWithCustomNewValueVersionIriRequest( + resourceIri = resourceIri, + valueIri = intValueForRsyncIri.get, + intValue = intValue, + newValueVersionIri = newValueVersionIri + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + val responseAsString = responseToString(response) + assert(response.status == StatusCodes.BadRequest, responseAsString) + } + + "not update an integer value with a custom new value version IRI that refers to the wrong project code" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val intValue: Int = 8 + val newValueVersionIri: IRI = "http://rdfh.ch/0002/a-thing/values/foo" + + val jsonLdEntity = SharedTestDataADM.updateIntValueWithCustomNewValueVersionIriRequest( + resourceIri = resourceIri, + valueIri = intValueForRsyncIri.get, + intValue = intValue, + newValueVersionIri = newValueVersionIri + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + val responseAsString = responseToString(response) + assert(response.status == StatusCodes.BadRequest, responseAsString) + } + + "not update an integer value with a custom new value version IRI that refers to the wrong resource" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val intValue: Int = 8 + val newValueVersionIri: IRI = "http://rdfh.ch/0001/nResNuvARcWYUdWyo0GWGw/values/foo" + + val jsonLdEntity = SharedTestDataADM.updateIntValueWithCustomNewValueVersionIriRequest( + resourceIri = resourceIri, + valueIri = intValueForRsyncIri.get, + intValue = intValue, + newValueVersionIri = newValueVersionIri + ) + + val request = Put(baseApiUrl + "/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + val responseAsString = responseToString(response) + assert(response.status == StatusCodes.BadRequest, responseAsString) + } + "not update an integer value if the simple schema is submitted" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri val intValue: Int = 10 @@ -2137,13 +2244,13 @@ class ValuesRouteV2E2ESpec extends E2ESpec { "update an integer value with custom permissions" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri - val intValue: Int = 7 + val intValue: Int = 3879 val customPermissions: String = "CR http://rdfh.ch/groups/0001/thing-searcher" val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) val jsonLDEntity = SharedTestDataADM.updateIntValueWithCustomPermissionsRequest( resourceIri = resourceIri, - valueIri = intValueIri.get, + valueIri = intValueWithCustomPermissionsIri.get, intValue = intValue, permissions = customPermissions ) @@ -2153,7 +2260,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { assert(response.status == StatusCodes.OK, response.toString) val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) val valueIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) - intValueIri.set(valueIri) + intValueWithCustomPermissionsIri.set(valueIri) val valueType: SmartIri = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) valueType should ===(OntologyConstants.KnoraApiV2Complex.IntValue.toSmartIri) @@ -2162,7 +2269,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = propertyIri, propertyIriInResult = propertyIri, - expectedValueIri = intValueIri.get, + expectedValueIri = intValueWithCustomPermissionsIri.get, userEmail = anythingUserEmail ) @@ -3176,7 +3283,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val jsonLdEntity = SharedTestDataADM.deleteIntValueRequestWithCustomDeleteDate( resourceIri = SharedTestDataADM.AThing.iri, - valueIri = intValueIriWithCustomCreationDate.get, + valueIri = intValueForRsyncIri.get, deleteDate = deleteDate ) 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 c568cfb8d5..42b29d4f0b 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 @@ -77,7 +77,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { private val firstIntValueVersionIri = new MutableTestIri private val intValueIri = new MutableTestIri private val intValueIriWithCustomPermissions = new MutableTestIri - private val intValueIriWithCustomUuidAndTimestamp = new MutableTestIri + private val intValueForRsyncIri = new MutableTestIri private val zeitglöckleinCommentWithoutStandoffIri = new MutableTestIri private val zeitglöckleinCommentWithStandoffIri = new MutableTestIri private val zeitglöckleinCommentWithCommentIri = new MutableTestIri @@ -862,7 +862,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { ) expectMsgPF(timeout) { - case createValueResponse: CreateValueResponseV2 => intValueIriWithCustomUuidAndTimestamp.set(createValueResponse.valueIri) + case createValueResponse: CreateValueResponseV2 => intValueForRsyncIri.set(createValueResponse.valueIri) } // Read the value back to check that it was added correctly. @@ -872,7 +872,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = propertyIri, propertyIriInResult = propertyIri, - expectedValueIri = intValueIriWithCustomUuidAndTimestamp.get, + expectedValueIri = intValueForRsyncIri.get, requestingUser = anythingUser1 ) @@ -897,7 +897,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { resourceIri = resourceIri, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, propertyIri = propertyIri, - valueIri = intValueIriWithCustomUuidAndTimestamp.get, + valueIri = intValueForRsyncIri.get, valueContent = IntegerValueContentV2( ontologySchema = ApiV2Complex, valueHasInteger = intValue @@ -918,33 +918,73 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { 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( + // Update the value. + + val intValue = 988 + val valueCreationDate = Instant.now + + responderManager ! UpdateValueRequestV2( + UpdateValueContentV2( + resourceIri = resourceIri, + resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + propertyIri = propertyIri, + valueIri = intValueForRsyncIri.get, + valueContent = IntegerValueContentV2( + ontologySchema = ApiV2Complex, + valueHasInteger = intValue + ), + valueCreationDate = Some(valueCreationDate) + ), + requestingUser = anythingUser1, + apiRequestID = UUID.randomUUID + ) + + expectMsgPF(timeout) { + case updateValueResponse: UpdateValueResponseV2 => + intValueForRsyncIri.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 = intValueIriWithCustomUuidAndTimestamp.get, - requestingUser = anythingUser1, - checkLastModDateChanged = false + expectedValueIri = intValueForRsyncIri.get, + requestingUser = anythingUser1 ) + updatedValueFromTriplestore.valueContent match { + case savedValue: IntegerValueContentV2 => + savedValue.valueHasInteger should ===(intValue) + updatedValueFromTriplestore.valueCreationDate should ===(valueCreationDate) + + case _ => throw AssertionException(s"Expected integer value, got $updatedValueFromTriplestore") + } + } + + "update an integer value with a custom new version IRI" 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) + // Update the value. - val intValue = 988 - val valueCreationDate = Instant.now + val intValue = 1000 + val newValueVersionIri: IRI = stringFormatter.makeRandomValueIri(resourceIri) responderManager ! UpdateValueRequestV2( UpdateValueContentV2( resourceIri = resourceIri, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, propertyIri = propertyIri, - valueIri = intValueIriWithCustomUuidAndTimestamp.get, + valueIri = intValueForRsyncIri.get, valueContent = IntegerValueContentV2( ontologySchema = ApiV2Complex, valueHasInteger = intValue ), - valueCreationDate = Some(valueCreationDate) + newValueVersionIri = Some(newValueVersionIri.toSmartIri) ), requestingUser = anythingUser1, apiRequestID = UUID.randomUUID @@ -952,7 +992,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { expectMsgPF(timeout) { case updateValueResponse: UpdateValueResponseV2 => - intValueIriWithCustomUuidAndTimestamp.set(updateValueResponse.valueIri) + intValueForRsyncIri.set(updateValueResponse.valueIri) } // Read the value back to check that it was added correctly. @@ -962,14 +1002,14 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = propertyIri, propertyIriInResult = propertyIri, - expectedValueIri = intValueIriWithCustomUuidAndTimestamp.get, + expectedValueIri = intValueForRsyncIri.get, requestingUser = anythingUser1 ) updatedValueFromTriplestore.valueContent match { case savedValue: IntegerValueContentV2 => savedValue.valueHasInteger should ===(intValue) - updatedValueFromTriplestore.valueCreationDate should ===(valueCreationDate) + updatedValueFromTriplestore.valueIri should ===(newValueVersionIri) case _ => throw AssertionException(s"Expected integer value, got $updatedValueFromTriplestore") } @@ -4165,7 +4205,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { resourceIri = resourceIri, resourceClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, propertyIri = propertyIri, - valueIri = intValueIriWithCustomUuidAndTimestamp.get, + valueIri = intValueForRsyncIri.get, valueTypeIri = OntologyConstants.KnoraApiV2Complex.IntValue.toSmartIri, deleteComment = Some("this value was incorrect"), deleteDate = Some(deleteDate), @@ -4180,7 +4220,7 @@ class ValuesResponderV2Spec extends CoreSpec() with ImplicitSender { maybePreviousLastModDate = maybeResourceLastModDate, propertyIriForGravsearch = propertyIri, propertyIriInResult = propertyIri, - valueIri = intValueIriWithCustomUuidAndTimestamp.get, + valueIri = intValueForRsyncIri.get, customDeleteDate = Some(deleteDate), requestingUser = anythingUser1 )