diff --git a/docs/src/paradox/03-apis/api-v2/editing-resources.md b/docs/src/paradox/03-apis/api-v2/editing-resources.md index 2f82701283..5f1b05233f 100644 --- a/docs/src/paradox/03-apis/api-v2/editing-resources.md +++ b/docs/src/paradox/03-apis/api-v2/editing-resources.md @@ -32,8 +32,8 @@ HTTP POST to http://host/v2/resources The body of the request is a JSON-LD document in the @ref:[complex API schema](introduction.md#api-schema), specifying the resource's IRI, type, and `rdfs:label`, along with its Knora resource properties and their values. The representation of the -resource is the same as when it is returned in a `GET` request, except that its IRI and -`knora-api:attachedToUser`, and those of its values, are not given. The format of the values submitted +resource is the same as when it is returned in a `GET` request, except that its `knora-api:attachedToUser` is not given, +and the resource IRI and those of its values can be optionally specified. The format of the values submitted is described in @ref:[Editing Values](editing-values.md). If there are multiple values for a property, these must be given in an array. @@ -160,7 +160,7 @@ resource's creator can be specfied by adding `knora-api:attachedToUser`. For exa "knora-api:creationDate" : { "@type" : "xsd:dateTimeStamp", "@value" : "2019-01-09T15:45:54.502951Z" - } + }, "@context" : { "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", @@ -185,6 +185,39 @@ than the requesting user only if the requesting user is an administrator of the 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 for a resource through +the `@id` attribute which will then be assigned to the resource; otherwise the resource will get a unique random IRI. +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`. +Each custom UUID must be [base64url-encoded](rfc:4648#section-5), without padding. For example: +```jsonld +{ + "@id" : "http://rdfh.ch/0001/a-custom-thing", + "@type" : "anything:Thing", + "knora-api:attachedToProject" : { + "@id" : "http://rdfh.ch/projects/0001" + }, + "anything:hasInteger" : { + "@id" : "http://rdfh.ch/0001/a-thing/values/int-value-IRI", + "@type" : "knora-api:IntValue", + "knora-api:intValueAsInt" : 10, + "knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ" + }, + "rdfs:label" : "test thing with custom IRI", + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2019-01-09T15:45:54.502951Z" + }, + "@context" : { + "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + "xsd" : "http://www.w3.org/2001/XMLSchema#", + "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + } +} +``` + The response is a JSON-LD document containing a @ref:[preview](reading-and-searching-resources.md#get-the-preview-of-a-resource-by-its-iri) of the resource. diff --git a/docs/src/paradox/03-apis/api-v2/editing-values.md b/docs/src/paradox/03-apis/api-v2/editing-values.md index 17d9008e06..00fb470650 100644 --- a/docs/src/paradox/03-apis/api-v2/editing-values.md +++ b/docs/src/paradox/03-apis/api-v2/editing-values.md @@ -85,7 +85,31 @@ Permissions for the new value can be given by adding `knora-api:hasPermissions`. } } ``` +Each value can have an optional custom IRI specified by the `@id` attribute, a custom creation date specified by adding +`knora-api:creationDate` (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: + +```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", + "@type" : "knora-api:IntValue", + "knora-api:intValueAsInt" : 21, + "knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ", + "knora-api:creationDate" : { + "@type" : "xsd:dateTimeStamp", + "@value" : "2020-06-04T12:58:54.502951Z" + } + }, + "@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#" + } +``` The format of the object of `knora-api:hasPermissions` is described in @ref:[Permissions](../../02-knora-ontologies/knora-base.md#permissions). diff --git a/webapi/src/it/scala/org/knora/webapi/e2e/v2/KnoraSipiIntegrationV2ITSpec.scala b/webapi/src/it/scala/org/knora/webapi/e2e/v2/KnoraSipiIntegrationV2ITSpec.scala index d5224e7312..1f130c8295 100644 --- a/webapi/src/it/scala/org/knora/webapi/e2e/v2/KnoraSipiIntegrationV2ITSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/e2e/v2/KnoraSipiIntegrationV2ITSpec.scala @@ -356,12 +356,12 @@ class KnoraSipiIntegrationV2ITSpec extends ITKnoraLiveSpec(KnoraSipiIntegrationV val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) val responseJsonDoc: JsonLDDocument = getResponseJsonLD(request) - stillImageResourceIri.set(responseJsonDoc.body.getIDAsKnoraDataIri.toString) + stillImageResourceIri.set(responseJsonDoc.body.requireIDAsKnoraDataIri.toString) // Get the resource from Knora. val knoraGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(stillImageResourceIri.get, "UTF-8")}") val resource: JsonLDDocument = getResponseJsonLD(knoraGetRequest) - assert(resource.getTypeAsKnoraTypeIri.toString == "http://0.0.0.0:3333/ontology/0001/anything/v2#ThingPicture") + assert(resource.requireTypeAsKnoraTypeIri.toString == "http://0.0.0.0:3333/ontology/0001/anything/v2#ThingPicture") // Get the new file value from the resource. @@ -381,7 +381,7 @@ class KnoraSipiIntegrationV2ITSpec extends ITKnoraLiveSpec(KnoraSipiIntegrationV case other => throw AssertionException(s"Invalid value object: $other") } - stillImageFileValueIri.set(savedValueObj.getIDAsKnoraDataIri.toString) + stillImageFileValueIri.set(savedValueObj.requireIDAsKnoraDataIri.toString) val savedImage = savedValueToSavedImage(savedValueObj) assert(savedImage.internalFilename == uploadedFile.internalFilename) @@ -430,7 +430,7 @@ class KnoraSipiIntegrationV2ITSpec extends ITKnoraLiveSpec(KnoraSipiIntegrationV // Send the JSON in a PUT request to Knora. val knoraPostRequest = Put(baseApiUrl + "/v2/values", HttpEntity(ContentTypes.`application/json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) val responseJsonDoc = getResponseJsonLD(knoraPostRequest) - stillImageFileValueIri.set(responseJsonDoc.body.getIDAsKnoraDataIri.toString) + stillImageFileValueIri.set(responseJsonDoc.body.requireIDAsKnoraDataIri.toString) // Get the resource from Knora. val knoraGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(stillImageResourceIri.get, "UTF-8")}") @@ -523,12 +523,12 @@ class KnoraSipiIntegrationV2ITSpec extends ITKnoraLiveSpec(KnoraSipiIntegrationV val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) val responseJsonDoc: JsonLDDocument = getResponseJsonLD(request) - pdfResourceIri.set(responseJsonDoc.body.getIDAsKnoraDataIri.toString) + pdfResourceIri.set(responseJsonDoc.body.requireIDAsKnoraDataIri.toString) // Get the resource from Knora. val knoraGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(pdfResourceIri.get, "UTF-8")}") val resource: JsonLDDocument = getResponseJsonLD(knoraGetRequest) - assert(resource.getTypeAsKnoraTypeIri.toString == "http://0.0.0.0:3333/ontology/0001/anything/v2#ThingDocument") + assert(resource.requireTypeAsKnoraTypeIri.toString == "http://0.0.0.0:3333/ontology/0001/anything/v2#ThingDocument") // Get the new file value from the resource. @@ -548,7 +548,7 @@ class KnoraSipiIntegrationV2ITSpec extends ITKnoraLiveSpec(KnoraSipiIntegrationV case other => throw AssertionException(s"Invalid value object: $other") } - pdfValueIri.set(savedValueObj.getIDAsKnoraDataIri.toString) + pdfValueIri.set(savedValueObj.requireIDAsKnoraDataIri.toString) val savedDocument: SavedDocument = savedValueToSavedDocument(savedValueObj) assert(savedDocument.internalFilename == uploadedFile.internalFilename) @@ -589,7 +589,7 @@ class KnoraSipiIntegrationV2ITSpec extends ITKnoraLiveSpec(KnoraSipiIntegrationV val request = Put(s"$baseApiUrl/v2/values", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLdEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) val responseJsonDoc: JsonLDDocument = getResponseJsonLD(request) - pdfValueIri.set(responseJsonDoc.body.getIDAsKnoraDataIri.toString) + pdfValueIri.set(responseJsonDoc.body.requireIDAsKnoraDataIri.toString) // Get the resource from Knora. val knoraGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(pdfResourceIri.get, "UTF-8")}") diff --git a/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala b/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala index 2674255b9c..3a6759327a 100644 --- a/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/SharedTestDataADM.scala @@ -402,6 +402,13 @@ object SharedTestDataADM { /** **********************************/ val ANYTHING_PROJECT_IRI = "http://rdfh.ch/projects/0001" + val customResourceIRI: IRI = "http://rdfh.ch/0001/a-thing-with-IRI" + val customResourceIRI_resourceWithValues: IRI = "http://rdfh.ch/0001/a-thing-with-value-IRI" + val customValueIRI_withResourceIriAndValueIRIAndValueUUID: IRI = "http://rdfh.ch/0001/a-thing-with-value-IRI/values/a-value-with-IRI-and-UUID" + val customValueUUID = "IN4R19yYR0ygi3K2VEHpUQ" + val customValueIRI: IRI = "http://rdfh.ch/0001/a-thing-with-value-IRI/values/a-value-with-IRI" + val customResourceCreationDate: Instant = Instant.parse("2019-01-09T15:45:54.502951Z") + val customValueCreationDate: Instant = Instant.parse("2020-06-09T17:04:54.502951Z") def anythingAdminUser: UserADM = UserADM( id = "http://rdfh.ch/users/AnythingAdminUser", @@ -676,6 +683,88 @@ object SharedTestDataADM { |}""".stripMargin } + def createIntValueWithCustomIRIRequest(resourceIri: IRI, + intValue: Int, + valueIri: IRI, + valueUUID: String, + valueCreationDate: Instant): String = { + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasInteger" : { + | "@id" : "$valueIri", + | "@type" : "knora-api:IntValue", + | "knora-api:intValueAsInt" : $intValue, + | "knora-api:valueHasUUID" : "$valueUUID", + | "knora-api:creationDate" : { + | "@type" : "xsd:dateTimeStamp", + | "@value" : "$valueCreationDate" + | } + | }, + | "@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 createIntValueWithCustomUUIDRequest(resourceIri: IRI, + intValue: Int, + valueUUID: String): String = { + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasInteger" : { + | "@type" : "knora-api:IntValue", + | "knora-api:intValueAsInt" : $intValue, + | "knora-api:valueHasUUID" : "$valueUUID" + | }, + | "@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 createIntValueWithCustomValueIriRequest(resourceIri: IRI, intValue: Int, valueIri: IRI): String = { + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasInteger" : { + | "@id" : "$valueIri", + | "@type" : "knora-api:IntValue", + | "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 createIntValueWithCustomCreationDateRequest(resourceIri: IRI, intValue: Int, creationDate: Instant): String = { + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasInteger" : { + | "@type" : "knora-api:IntValue", + | "knora-api:intValueAsInt" : $intValue, + | "knora-api:creationDate" : { + | "@type" : "xsd:dateTimeStamp", + | "@value" : "$creationDate" + | } + | }, + | "@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 createIntValueWithCustomPermissionsRequest(resourceIri: IRI, intValue: Int, customPermissions: String): String = { s"""{ | "@id" : "$resourceIri", @@ -1048,6 +1137,35 @@ object SharedTestDataADM { } } + def createLinkValueWithCustomIriRequest(resourceIri: IRI, + targetResourceIri: IRI, + customValueIri: IRI, + customValueUUID: String, + customValueCreationDate: Instant): String = { + s"""{ + | "@id" : "$resourceIri", + | "@type" : "anything:Thing", + | "anything:hasOtherThingValue" : { + | "@id" : "$customValueIri", + | "@type" : "knora-api:LinkValue", + | "knora-api:valueHasUUID": "IN4R19yYR0ygi3K2VEHpUQ", + | "knora-api:linkValueHasTargetIri" : { + | "@id" : "$targetResourceIri" + | }, + | "knora-api:creationDate" : { + | "@type" : "xsd:dateTimeStamp", + | "@value" : "$customValueCreationDate" + | } + | }, + | "@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, valueIri: IRI, intValue: Int): String = { @@ -1751,6 +1869,129 @@ object SharedTestDataADM { |}""".stripMargin } + def createResourceWithCustomIRI(customIRI: IRI): String = { + s"""{ + | "@id" : "$customIRI", + | "@type" : "anything:Thing", + | "knora-api:attachedToProject" : { + | "@id" : "http://rdfh.ch/projects/0001" + | }, + | "anything:hasBoolean" : { + | "@type" : "knora-api:BooleanValue", + | "knora-api:booleanValueAsBoolean" : true + | }, + | "rdfs:label" : "test thing", + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + } + + def createResourceWithCustomValueIRI(customValueIRI: IRI): String = { + s"""{ + | "@type" : "anything:Thing", + | "knora-api:attachedToProject" : { + | "@id" : "http://rdfh.ch/projects/0001" + | }, + | "anything:hasBoolean" : { + | "@id" : "$customValueIRI", + | "@type" : "knora-api:BooleanValue", + | "knora-api:booleanValueAsBoolean" : true + | }, + | "rdfs:label" : "test thing with value IRI", + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + } + + def createResourceWithCustomValueUUID(customValueUUID: String): String = { + s"""{ + | "@type" : "anything:Thing", + | "knora-api:attachedToProject" : { + | "@id" : "http://rdfh.ch/projects/0001" + | }, + | "anything:hasBoolean" : { + | "@type" : "knora-api:BooleanValue", + | "knora-api:booleanValueAsBoolean" : true, + | "knora-api:valueHasUUID" : "$customValueUUID" + | }, + | "rdfs:label" : "test thing", + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + } + + def createResourceWithCustomValueCreationDate(creationDate: Instant): String = { + s"""{ + | "@type" : "anything:Thing", + | "knora-api:attachedToProject" : { + | "@id" : "http://rdfh.ch/projects/0001" + | }, + | "anything:hasBoolean" : { + | "@type" : "knora-api:BooleanValue", + | "knora-api:booleanValueAsBoolean" : false, + | "knora-api:creationDate" : { + | "@type" : "xsd:dateTimeStamp", + | "@value" : "$creationDate" + | } + | }, + | "rdfs:label" : "test thing with value has creation date", + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + } + + + def createResourceWithCustomResourceIriAndCreationDateAndValueWithCustomIRIAndUUID(customResourceIRI: IRI, + customCreationDate: Instant, + customValueIRI: IRI, + customValueUUID: String): String = { + s"""{ + | "@id" : "$customResourceIRI", + | "@type" : "anything:Thing", + | "knora-api:attachedToProject" : { + | "@id" : "http://rdfh.ch/projects/0001" + | }, + | "anything:hasBoolean" : { + | "@id": "$customValueIRI", + | "@type" : "knora-api:BooleanValue", + | "knora-api:booleanValueAsBoolean" : true, + | "knora-api:valueHasUUID" : "$customValueUUID" + | }, + | "rdfs:label" : "test thing", + | "knora-api:creationDate" : { + | "@type" : "xsd:dateTimeStamp", + | "@value" : "$customCreationDate" + | }, + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "anything" : "http://0.0.0.0:3333/ontology/0001/anything/v2#" + | } + |}""".stripMargin + } + def createResourceAsUser(userADM: UserADM): String = { s"""{ | "@type" : "anything:Thing", @@ -2345,4 +2586,5 @@ object SharedTestDataADM { val testResponseValueIri: IRI = "http://rdfh.ch/0001/_GlNQXdYRTyQPhpdh76U1w/values/OGbYaSgNSUCKQtmn9suXlw" val testResponseValueUUID: UUID = UUID.fromString("84a3af57-ee99-486f-aa9c-e4ca1d19a57d") + val testResponseValueCreationDate: Instant = Instant.parse("2019-01-09T15:45:54.502951Z") } 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 e9b20a4d9b..0adb2a18e3 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 @@ -25,6 +25,7 @@ import java.util.UUID import akka.actor.ActorRef import akka.event.LoggingAdapter +import akka.http.scaladsl.util.FastFuture import akka.pattern._ import akka.util.Timeout import org.eclipse.rdf4j.rio.rdfxml.util.RDFXMLPrettyWriter @@ -440,10 +441,16 @@ case class ReadResourceV2(resourceIri: IRI, /** * The value of a Knora property sent to Knora to be created in a new resource. * - * @param valueContent the content of the new value. If the client wants to create a link, this must be a [[LinkValueContentV2]]. - * @param permissions the permissions to be given to the new value. If not provided, these will be taken from defaults. + * @param valueContent the content of the new value. If the client wants to create a link, this must be a [[LinkValueContentV2]]. + * @param customValueIri the optional custom value IRI. + * @param customValueUUID the optional custom value UUID. + * @param customValueCreationDate the optional custom value creation date. + * @param permissions the permissions to be given to the new value. If not provided, these will be taken from defaults. */ case class CreateValueInNewResourceV2(valueContent: ValueContentV2, + customValueIri: Option[SmartIri] = None, + customValueUUID: Option[UUID] = None, + customValueCreationDate: Option[Instant] = None, permissions: Option[String] = None) extends IOValueV2 /** @@ -525,7 +532,10 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource for { // Get the resource class. - resourceClassIri: SmartIri <- Future(jsonLDDocument.getTypeAsKnoraTypeIri) + resourceClassIri: SmartIri <- Future(jsonLDDocument.requireTypeAsKnoraTypeIri) + + // Get the custom resource IRI if provided. + maybeCustomResourceIri: Option[SmartIri] = jsonLDDocument.maybeIDAsKnoraDataIri // Get the resource's rdfs:label. label: String = jsonLDDocument.requireStringWithValidation(OntologyConstants.Rdfs.Label, stringFormatter.toSparqlEncodedString) @@ -595,14 +605,21 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource settings = settings, log = log ) - - _ = if (valueJsonLDObject.value.get(JsonLDConstants.ID).nonEmpty) { - throw BadRequestException("The @id of a value cannot be given in a request to create the value") - } - + maybeCustomValueIri: Option[SmartIri] = valueJsonLDObject.maybeIDAsKnoraDataIri + maybeCustomValueUUID: Option[UUID] = valueJsonLDObject.maybeUUID + + // Get the values's creation date. + maybeCustomValueCreationDate: Option[Instant] = valueJsonLDObject.maybeDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.CreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) maybePermissions: Option[String] = valueJsonLDObject.maybeStringWithValidation(OntologyConstants.KnoraApiV2Complex.HasPermissions, stringFormatter.toSparqlEncodedString) } yield CreateValueInNewResourceV2( valueContent = valueContent, + customValueIri = maybeCustomValueIri, + customValueUUID = maybeCustomValueUUID, + customValueCreationDate = maybeCustomValueCreationDate, permissions = maybePermissions ) } @@ -618,8 +635,13 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource requestingUser = requestingUser )).mapTo[ProjectGetResponseADM] - // Generate a random IRI for the resource. - resourceIri <- stringFormatter.makeUnusedIri(stringFormatter.makeRandomResourceIri(projectInfoResponse.project.shortcode), storeManager, log) + // If no custom IRI was provided, generate a random IRI for the resource. + // TODO: move this logic into ResourcesResponderV2 and run it while holding a lock on the IRI. + resourceIri: IRI <- maybeCustomResourceIri match { + case Some(customResourceIri) => FastFuture.successful(customResourceIri.toString) + case None => stringFormatter.makeUnusedIri(stringFormatter.makeRandomResourceIri(projectInfoResponse.project.shortcode), storeManager, log) + } + } yield CreateResourceRequestV2( createResource = CreateResourceV2( resourceIri = resourceIri, @@ -689,13 +711,13 @@ object UpdateResourceMetadataRequestV2 extends KnoraJsonLDRequestReaderV2[Update def fromJsonLDSync(jsonLDDocument: JsonLDDocument, requestingUser: UserADM, apiRequestID: UUID): UpdateResourceMetadataRequestV2 = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - val resourceIri: SmartIri = jsonLDDocument.getIDAsKnoraDataIri + val resourceIri: SmartIri = jsonLDDocument.requireIDAsKnoraDataIri if (!resourceIri.isKnoraResourceIri) { throw BadRequestException(s"Invalid resource IRI: <$resourceIri>") } - val resourceClassIri: SmartIri = jsonLDDocument.getTypeAsKnoraTypeIri + val resourceClassIri: SmartIri = jsonLDDocument.requireTypeAsKnoraTypeIri val maybeLastModificationDate: Option[Instant] = jsonLDDocument.maybeDatatypeValueInObject( key = OntologyConstants.KnoraApiV2Complex.LastModificationDate, @@ -780,13 +802,13 @@ object DeleteOrEraseResourceRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteO def fromJsonLDSync(jsonLDDocument: JsonLDDocument, requestingUser: UserADM, apiRequestID: UUID): DeleteOrEraseResourceRequestV2 = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - val resourceIri: SmartIri = jsonLDDocument.getIDAsKnoraDataIri + val resourceIri: SmartIri = jsonLDDocument.requireIDAsKnoraDataIri if (!resourceIri.isKnoraResourceIri) { throw BadRequestException(s"Invalid resource IRI: <$resourceIri>") } - val resourceClassIri: SmartIri = jsonLDDocument.getTypeAsKnoraTypeIri + val resourceClassIri: SmartIri = jsonLDDocument.requireTypeAsKnoraTypeIri val maybeLastModificationDate: Option[Instant] = jsonLDDocument.maybeDatatypeValueInObject( key = OntologyConstants.KnoraApiV2Complex.LastModificationDate, 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 92111cec0b..f9fde2978a 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 @@ -90,21 +90,18 @@ object CreateValueRequestV2 extends KnoraJsonLDRequestReaderV2[CreateValueReques for { // Get the IRI of the resource that the value is to be created in. - resourceIri: SmartIri <- Future(jsonLDDocument.getIDAsKnoraDataIri) + resourceIri: SmartIri <- Future(jsonLDDocument.requireIDAsKnoraDataIri) _ = if (!resourceIri.isKnoraResourceIri) { throw BadRequestException(s"Invalid resource IRI: <$resourceIri>") } // Get the resource class. - resourceClassIri: SmartIri = jsonLDDocument.getTypeAsKnoraTypeIri + resourceClassIri: SmartIri = jsonLDDocument.requireTypeAsKnoraTypeIri // Get the resource property and the value to be created. - createValue: CreateValueV2 <- jsonLDDocument.getResourcePropertyValue match { + createValue: CreateValueV2 <- jsonLDDocument.requireResourcePropertyValue match { case (propertyIri: SmartIri, jsonLDObject: JsonLDObject) => - if (jsonLDObject.value.get(JsonLDConstants.ID).nonEmpty) { - throw BadRequestException("The @id of a value cannot be given in a request to create the value") - } for { valueContent: ValueContentV2 <- @@ -117,12 +114,27 @@ object CreateValueRequestV2 extends KnoraJsonLDRequestReaderV2[CreateValueReques log = log ) + // Get the custom value IRI if provided. + maybeCustomValueIri: Option[SmartIri] = jsonLDObject.maybeIDAsKnoraDataIri + + // Get the custom value UUID if provided. + maybeCustomUUID: Option[UUID] = jsonLDObject.maybeUUID + + // Get the value's creation date. + maybeCreationDate: Option[Instant] = jsonLDObject.maybeDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.CreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) maybePermissions: Option[String] = jsonLDObject.maybeStringWithValidation(OntologyConstants.KnoraApiV2Complex.HasPermissions, stringFormatter.toSparqlEncodedString) } yield CreateValueV2( resourceIri = resourceIri.toString, resourceClassIri = resourceClassIri, propertyIri = propertyIri, valueContent = valueContent, + customValueIri = maybeCustomValueIri, + customValueUUID = maybeCustomUUID, + customValueCreationDate = maybeCreationDate, permissions = maybePermissions ) } @@ -137,14 +149,16 @@ object CreateValueRequestV2 extends KnoraJsonLDRequestReaderV2[CreateValueReques /** * Represents a successful response to a [[CreateValueRequestV2]]. * - * @param valueIri the IRI of the value that was created. - * @param valueType the type of the value that was created. - * @param valueUUID the value's UUID. - * @param projectADM the project in which the value was created. + * @param valueIri the IRI of the value that was created. + * @param valueType the type of the value that was created. + * @param valueUUID the value's UUID. + * @param valueCreationDate the value's creationDate + * @param projectADM the project in which the value was created. */ case class CreateValueResponseV2(valueIri: IRI, valueType: SmartIri, valueUUID: UUID, + valueCreationDate: Instant, projectADM: ProjectADM) extends KnoraResponseV2 with UpdateResultInProject { override def toJsonLDDocument(targetSchema: ApiV2Schema, settings: KnoraSettingsImpl, schemaOptions: Set[SchemaOption]): JsonLDDocument = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -158,7 +172,11 @@ case class CreateValueResponseV2(valueIri: IRI, Map( JsonLDConstants.ID -> JsonLDString(valueIri), JsonLDConstants.TYPE -> JsonLDString(valueType.toOntologySchema(ApiV2Complex).toString), - OntologyConstants.KnoraApiV2Complex.ValueHasUUID -> JsonLDString(stringFormatter.base64EncodeUuid(valueUUID)) + OntologyConstants.KnoraApiV2Complex.ValueHasUUID -> JsonLDString(stringFormatter.base64EncodeUuid(valueUUID)), + OntologyConstants.KnoraApiV2Complex.ValueCreationDate -> JsonLDUtil.datatypeValueToJsonLDObject( + value = valueCreationDate.toString, + datatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri + ) ) ), context = JsonLDUtil.makeContext( @@ -210,19 +228,19 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques for { // Get the IRI of the resource that the value is to be created in. - resourceIri: SmartIri <- Future(jsonLDDocument.getIDAsKnoraDataIri) + resourceIri: SmartIri <- Future(jsonLDDocument.requireIDAsKnoraDataIri) _ = if (!resourceIri.isKnoraResourceIri) { throw BadRequestException(s"Invalid resource IRI: <$resourceIri>") } // Get the resource class. - resourceClassIri: SmartIri = jsonLDDocument.getTypeAsKnoraTypeIri + resourceClassIri: SmartIri = jsonLDDocument.requireTypeAsKnoraTypeIri // Get the resource property and the new value version. - updateValue: UpdateValueV2 <- jsonLDDocument.getResourcePropertyValue match { + updateValue: UpdateValueV2 <- jsonLDDocument.requireResourcePropertyValue match { case (propertyIri: SmartIri, jsonLDObject: JsonLDObject) => - val valueIri: IRI = jsonLDObject.getIDAsKnoraDataIri.toString + val valueIri: IRI = jsonLDObject.requireIDAsKnoraDataIri.toString // Does the value object just contain knora-api:hasPermissions? @@ -371,25 +389,25 @@ object DeleteValueRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteValueReques implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance // Get the IRI of the resource that the value is to be created in. - val resourceIri: SmartIri = jsonLDDocument.getIDAsKnoraDataIri + val resourceIri: SmartIri = jsonLDDocument.requireIDAsKnoraDataIri if (!resourceIri.isKnoraResourceIri) { throw BadRequestException(s"Invalid resource IRI: <$resourceIri>") } // Get the resource class. - val resourceClassIri: SmartIri = jsonLDDocument.getTypeAsKnoraTypeIri + val resourceClassIri: SmartIri = jsonLDDocument.requireTypeAsKnoraTypeIri // Get the resource property and the IRI and class of the value to be deleted. - jsonLDDocument.getResourcePropertyValue match { + jsonLDDocument.requireResourcePropertyValue match { case (propertyIri: SmartIri, jsonLDObject: JsonLDObject) => - val valueIri = jsonLDObject.getIDAsKnoraDataIri + val valueIri = jsonLDObject.requireIDAsKnoraDataIri if (!valueIri.isKnoraValueIri) { throw BadRequestException(s"Invalid value IRI: <$valueIri>") } - val valueTypeIri: SmartIri = jsonLDObject.getTypeAsKnoraApiV2ComplexTypeIri + val valueTypeIri: SmartIri = jsonLDObject.requireTypeAsKnoraApiV2ComplexTypeIri val deleteComment: Option[String] = jsonLDObject.maybeStringWithValidation(OntologyConstants.KnoraApiV2Complex.DeleteComment, stringFormatter.toSparqlEncodedString) @@ -438,6 +456,9 @@ case class GenerateSparqlToCreateMultipleValuesRequestV2(resourceIri: IRI, } case class GenerateSparqlForValueInNewResourceV2(valueContent: ValueContentV2, + customValueIri: Option[SmartIri], + customValueUUID: Option[UUID], + customValueCreationDate: Option[Instant], permissions: String) extends IOValueV2 /** @@ -762,16 +783,22 @@ case class ReadOtherValueV2(valueIri: IRI, /** * Represents a Knora value to be created in an existing resource. * - * @param resourceIri the resource the new value should be attached to. - * @param resourceClassIri the resource class that the client believes the resource belongs to. - * @param propertyIri the property of the new value. If the client wants to create a link, this must be a link value property. - * @param valueContent the content of the new value. If the client wants to create a link, this must be a [[LinkValueContentV2]]. - * @param permissions the permissions to be given to the new value. If not provided, these will be taken from defaults. + * @param resourceIri the resource the new value should be attached to. + * @param resourceClassIri the resource class that the client believes the resource belongs to. + * @param propertyIri the property of the new value. If the client wants to create a link, this must be a link value property. + * @param valueContent the content of the new value. If the client wants to create a link, this must be a [[LinkValueContentV2]]. + * @param customValueIri the optional custom IRI supplied for the value. + * @param customValueUUID the optional custom UUID supplied for the value. + * @param customValueCreationDate the optional custom creation date supplied for the value. + * @param permissions the permissions to be given to the new value. If not provided, these will be taken from defaults. */ case class CreateValueV2(resourceIri: IRI, resourceClassIri: SmartIri, propertyIri: SmartIri, valueContent: ValueContentV2, + customValueIri: Option[SmartIri] = None, + customValueUUID: Option[UUID] = None, + customValueCreationDate: Option[Instant] = None, permissions: Option[String] = None) extends IOValueV2 diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 13e688e152..f40c7fe93b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -925,6 +925,9 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt } } yield GenerateSparqlForValueInNewResourceV2( valueContent = valueToCreate.valueContent, + customValueIri = valueToCreate.customValueIri, + customValueUUID = valueToCreate.customValueUUID, + customValueCreationDate = valueToCreate.customValueCreationDate, permissions = validatedCustomPermissions ) @@ -933,6 +936,9 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt FastFuture.successful { GenerateSparqlForValueInNewResourceV2( valueContent = valueToCreate.valueContent, + customValueIri = valueToCreate.customValueIri, + customValueUUID = valueToCreate.customValueUUID, + customValueCreationDate = valueToCreate.customValueCreationDate, permissions = defaultPropertyPermissions(propertyIri) ) } 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 2c0fb9050d..cb4092636e 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 @@ -248,6 +248,9 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde resourceInfo = resourceInfo, propertyIri = adjustedInternalPropertyIri, value = submittedInternalValueContent, + customValueIri = createValueRequest.createValue.customValueIri, + customValueUUID = createValueRequest.createValue.customValueUUID, + customValueCreationDate = createValueRequest.createValue.customValueCreationDate, valueCreator = createValueRequest.requestingUser.id, valuePermissions = newValuePermissionLiteral, requestingUser = createValueRequest.requestingUser @@ -265,11 +268,13 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde valueIri = verifiedValue.newValueIri, valueType = verifiedValue.value.valueType, valueUUID = unverifiedValue.newValueUUID, + valueCreationDate = unverifiedValue.creationDate, projectADM = resourceInfo.projectADM ) } val triplestoreUpdateFuture: Future[CreateValueResponseV2] = for { + // Don't allow anonymous users to create values. _ <- Future { if (createValueRequest.requestingUser.isAnonymousUser) { @@ -309,13 +314,17 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde * * @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. + * @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 customValueIri the optional custom IRI supplied for the value. + * @param customValueUUID the optional custom UUID supplied for the value. + * @param customValueCreationDate the optional custom creation date supplied for 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 createValueV2AfterChecks(dataNamedGraph: IRI, @@ -323,9 +332,13 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde resourceInfo: ReadResourceV2, propertyIri: SmartIri, value: ValueContentV2, + customValueIri: Option[SmartIri], + customValueUUID: Option[UUID], + customValueCreationDate: Option[Instant], valueCreator: IRI, valuePermissions: String, requestingUser: UserADM): Future[UnverifiedValueV2] = { + value match { case linkValueContent: LinkValueContentV2 => createLinkValueV2AfterChecks( @@ -333,6 +346,9 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde resourceInfo = resourceInfo, linkPropertyIri = propertyIri, linkValueContent = linkValueContent, + maybeValueIri = customValueIri, + maybeValueUUID = customValueUUID, + maybeCreationDate = customValueCreationDate, valueCreator = valueCreator, valuePermissions = valuePermissions, requestingUser = requestingUser @@ -344,6 +360,9 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde resourceInfo = resourceInfo, propertyIri = propertyIri, value = ordinaryValueContent, + maybeValueIri = customValueIri, + maybeValueUUID = customValueUUID, + maybeValueCreationDate = customValueCreationDate, valueCreator = valueCreator, valuePermissions = valuePermissions, requestingUser = requestingUser @@ -354,27 +373,46 @@ 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. + * @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 maybeValueIri the optional custom IRI supplied for the value. + * @param maybeValueUUID the optional custom UUID supplied for the value. + * @param maybeValueCreationDate the optional custom creation date supplied for 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, value: ValueContentV2, + maybeValueIri: Option[SmartIri], + maybeValueUUID: Option[UUID], + maybeValueCreationDate: Option[Instant], valueCreator: IRI, valuePermissions: String, requestingUser: UserADM): Future[UnverifiedValueV2] = { for { - // Generate an IRI and a UUID for the new value. - newValueIri <- FastFuture.successful(stringFormatter.makeRandomValueIri(resourceInfo.resourceIri)) - newValueUUID = UUID.randomUUID - currentTime: Instant = Instant.now + // Make an IRI for the new value. + newValueIri: IRI <- maybeValueIri match { + case Some(customValueIri) => FastFuture.successful(customValueIri.toString) + case None => FastFuture.successful(stringFormatter.makeRandomValueIri(resourceInfo.resourceIri)) + } + + // Make a UUID for the new value + newValueUUID: UUID = maybeValueUUID match { + case Some(customValueUUID) => customValueUUID + case None => UUID.randomUUID + } + + // Make a creation date for the new value + creationDate: Instant = maybeValueCreationDate match { + case Some(customCreationDate) => customCreationDate + case None => Instant.now + } // If we're creating a text value, update direct links and LinkValues for any resource references in standoff. standoffLinkUpdates: Seq[SparqlTemplateLinkUpdate] = value match { @@ -407,7 +445,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde linkUpdates = standoffLinkUpdates, valueCreator = valueCreator, valuePermissions = valuePermissions, - creationDate = currentTime, + creationDate = creationDate, stringFormatter = stringFormatter ).toString() @@ -424,42 +462,55 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde newValueUUID = newValueUUID, valueContent = value.unescape, permissions = valuePermissions, - creationDate = currentTime + creationDate = creationDate ) } /** * 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. + * @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 maybeValueIri the optional custom IRI supplied for the value. + * @param maybeValueUUID the optional custom UUID supplied for the 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 an [[UnverifiedValueV2]]. */ private def createLinkValueV2AfterChecks(dataNamedGraph: IRI, resourceInfo: ReadResourceV2, linkPropertyIri: SmartIri, linkValueContent: LinkValueContentV2, + maybeValueIri: Option[SmartIri], + maybeValueUUID: Option[UUID], + maybeCreationDate: Option[Instant], valueCreator: IRI, valuePermissions: String, requestingUser: UserADM): Future[UnverifiedValueV2] = { - val newValueUUID = UUID.randomUUID + + val newValueUUID: UUID = maybeValueUUID match { + case Some(customValueUUID) => customValueUUID + case None => UUID.randomUUID + } for { sparqlTemplateLinkUpdate <- Future(incrementLinkValue( sourceResourceInfo = resourceInfo, linkPropertyIri = linkPropertyIri, targetResourceIri = linkValueContent.referredResourceIri, + customNewLinkValueIri = maybeValueIri, valueCreator = valueCreator, valuePermissions = valuePermissions, requestingUser = requestingUser )) - currentTime: Instant = Instant.now + creationDate: Instant = maybeCreationDate match { + case Some(customValueCreationDate) => customValueCreationDate + case None => Instant.now + } // Generate a SPARQL update string. sparqlUpdate = queries.sparql.v2.txt.createLink( @@ -468,7 +519,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde resourceIri = resourceInfo.resourceIri, linkUpdate = sparqlTemplateLinkUpdate, newValueUUID = newValueUUID, - creationDate = currentTime, + creationDate = creationDate, maybeComment = linkValueContent.comment, stringFormatter = stringFormatter ).toString() @@ -486,7 +537,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde newValueUUID = newValueUUID, valueContent = linkValueContent.unescape, permissions = valuePermissions, - creationDate = currentTime + creationDate = creationDate ) } @@ -508,6 +559,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde private def generateSparqToCreateMultipleValuesV2(createMultipleValuesRequest: GenerateSparqlToCreateMultipleValuesRequestV2): Future[GenerateSparqlToCreateMultipleValuesResponseV2] = { for { // Generate SPARQL to create links and LinkValues for standoff links in text values. + sparqlForStandoffLinks: String <- Future(generateInsertSparqlForStandoffLinksInMultipleValues(createMultipleValuesRequest)) // Generate SPARQL for each value. @@ -520,7 +572,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde propertyIri = propertyIri, valueToCreate = valueToCreate, valueHasOrder = valueHasOrder, - creationDate = createMultipleValuesRequest.creationDate, + resourceCreationDate = createMultipleValuesRequest.creationDate, requestingUser = createMultipleValuesRequest.requestingUser ) } @@ -542,23 +594,39 @@ 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. + * @param resourceIri the IRI of the resource. + * @param propertyIri the IRI of the property that will point to the value. + * @param valueToCreate the value to be created. + * @param valueHasOrder the value's `knora-base:valueHasOrder`. + * @param resourceCreationDate the 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, valueHasOrder: Int, - creationDate: Instant, + resourceCreationDate: Instant, requestingUser: UserADM): InsertSparqlWithUnverifiedValue = { - // Make an IRI and a UUID for the new value. - val newValueIri = stringFormatter.makeRandomValueIri(resourceIri) - val newValueUUID = UUID.randomUUID + + // Make an IRI for the new value. + val newValueIri: IRI = valueToCreate.customValueIri match { + case Some(customValueIri) => customValueIri.toString + case None => stringFormatter.makeRandomValueIri(resourceIri) + } + + // Make a UUID for the new value. + val newValueUUID: UUID = valueToCreate.customValueUUID match { + case Some(customValueUUID) => customValueUUID + case None => UUID.randomUUID + } + + // Make a creation date for the value. If a custom creation date is given for a value, consider that otherwise + // use resource creation date for the value. + val valueCreationDate: Instant = valueToCreate.customValueCreationDate match { + case Some(customValueCreationDate) => customValueCreationDate + case None => resourceCreationDate + } // Generate the SPARQL. val insertSparql: String = valueToCreate.valueContent match { @@ -586,7 +654,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde queries.sparql.v2.txt.generateInsertStatementsForCreateLink( resourceIri = resourceIri, linkUpdate = sparqlTemplateLinkUpdate, - creationDate = creationDate, + creationDate = valueCreationDate, newValueUUID = newValueUUID, maybeComment = valueToCreate.valueContent.comment, maybeValueHasOrder = Some(valueHasOrder), @@ -604,7 +672,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde linkUpdates = Seq.empty[SparqlTemplateLinkUpdate], // This is empty because we have to generate SPARQL for standoff links separately. valueCreator = requestingUser.id, valuePermissions = valueToCreate.permissions, - creationDate = creationDate, + creationDate = valueCreationDate, maybeValueHasOrder = Some(valueHasOrder), stringFormatter = stringFormatter ).toString() @@ -617,7 +685,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde newValueUUID = newValueUUID, valueContent = valueToCreate.valueContent.unescape, permissions = valueToCreate.permissions, - creationDate = creationDate + creationDate = valueCreationDate ) ) } @@ -1917,6 +1985,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde * @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. @@ -1925,6 +1994,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde private def incrementLinkValue(sourceResourceInfo: ReadResourceV2, linkPropertyIri: SmartIri, targetResourceIri: IRI, + customNewLinkValueIri: Option[SmartIri]=None, valueCreator: IRI, valuePermissions: String, requestingUser: UserADM): SparqlTemplateLinkUpdate = { @@ -1935,9 +2005,11 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde targetResourceIri = targetResourceIri ) - // Generate an IRI for the new LinkValue. - val newLinkValueIri = stringFormatter.makeRandomValueIri(sourceResourceInfo.resourceIri) - + // Make an IRI for the new LinkValue. + val newLinkValueIri: IRI = customNewLinkValueIri match { + case Some(customValueIri) => customValueIri.toString + case None => stringFormatter.makeRandomValueIri(sourceResourceInfo.resourceIri) + } maybeLinkValueInfo match { case Some(linkValueInfo) => // There's already a LinkValue for links between these two resources. Increment diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index fd65d9ac6d..8368a45840 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -114,7 +114,32 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) ), TestDataFileContent( filePath = TestDataFilePath.makeJsonPath("create-resource-with-custom-creation-date"), - text = SharedTestDataADM.createResourceWithCustomCreationDate(Instant.parse("2019-01-09T15:45:54.502951Z")) + text = SharedTestDataADM.createResourceWithCustomCreationDate(SharedTestDataADM.customResourceCreationDate) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-resource-with-custom-IRI-request"), + text = SharedTestDataADM.createResourceWithCustomIRI(SharedTestDataADM.customResourceIRI) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-resource-with-custom-value-IRI-request"), + text = SharedTestDataADM.createResourceWithCustomValueIRI(SharedTestDataADM.customValueIRI) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-resource-with-custom-value-UUID-request"), + text = SharedTestDataADM.createResourceWithCustomValueUUID(SharedTestDataADM.customValueIRI_withResourceIriAndValueIRIAndValueUUID) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-resource-with-custom-value-creationDate-request"), + text = SharedTestDataADM.createResourceWithCustomValueCreationDate(SharedTestDataADM.customValueCreationDate) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-resource-with-custom-resourceIRI-creationDate-ValueIri-ValueUUID-request"), + text = SharedTestDataADM.createResourceWithCustomResourceIriAndCreationDateAndValueWithCustomIRIAndUUID( + SharedTestDataADM.customResourceIRI_resourceWithValues, + SharedTestDataADM.customResourceCreationDate, + SharedTestDataADM.customValueIRI_withResourceIriAndValueIRIAndValueUUID, + SharedTestDataADM.customValueUUID + ) ), TestDataFileContent( filePath = TestDataFilePath.makeJsonPath("create-resource-as-user"), diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala index 676ee09596..7c5a3484db 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala @@ -348,6 +348,50 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit resourceIri = SharedTestDataADM.AThing.iri, targetResourceIri = "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw" ) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-int-value-with-custom-Iri-request"), + text = SharedTestDataADM.createIntValueWithCustomValueIriRequest( + resourceIri = SharedTestDataADM.AThing.iri, + intValue = 30, + valueIri = "http://rdfh.ch/0001/a-thing/values/int-with-valueIRI" + ) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-int-value-with-custom-UUID-request"), + text = SharedTestDataADM.createIntValueWithCustomUUIDRequest( + resourceIri = SharedTestDataADM.AThing.iri, + intValue = 45, + valueUUID = "IN4R19yYR0ygi3K2VEHpUQ" + ) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-int-value-with-custom-creationDate-request"), + text = SharedTestDataADM.createIntValueWithCustomCreationDateRequest( + resourceIri = SharedTestDataADM.AThing.iri, + intValue = 25, + creationDate = Instant.parse("2020-06-04T11:36:54.502951Z") + ) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-int-value-with-custom-Iri-UUID-CreationDate-request"), + text = SharedTestDataADM.createIntValueWithCustomIRIRequest( + resourceIri = SharedTestDataADM.AThing.iri, + intValue = 10, + valueIri = "http://rdfh.ch/0001/a-thing/values/int-with-IRI", + valueUUID = "IN4R19yYR0ygi3K2VEHpUQ", + valueCreationDate = Instant.parse("2020-06-04T12:58:54.502951Z") + ) + ), + TestDataFileContent( + filePath = TestDataFilePath.makeJsonPath("create-link-value-with-custom-Iri-UUID-CreationDate-request"), + text = SharedTestDataADM.createLinkValueWithCustomIriRequest( + resourceIri = SharedTestDataADM.AThing.iri, + targetResourceIri = "http://rdfh.ch/0001/A67ka6UQRHWf313tbhQBjw", + customValueIri = "http://rdfh.ch/0001/a-thing/values/link-Value-With-IRI", + customValueUUID = "IN4R19yYR0ygi3K2VEHpUQ", + customValueCreationDate = Instant.parse("2020-06-04T11:36:54.502951Z") + ) ) ) ) @@ -358,6 +402,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit valueIri = SharedTestDataADM.testResponseValueIri, valueType = OntologyConstants.KnoraApiV2Complex.IntValue.toSmartIri, valueUUID = SharedTestDataADM.testResponseValueUUID, + valueCreationDate = SharedTestDataADM.testResponseValueCreationDate, projectADM = SharedTestDataADM.anythingProject ) diff --git a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala index 05bf8c2872..1ca2e175b0 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/StringFormatter.scala @@ -40,7 +40,6 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.store.triplestoremessages.{SparqlAskRequest, SparqlAskResponse} import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1 import org.knora.webapi.messages.v2.responder.KnoraContentV2 -import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.util.IriConversions._ import org.knora.webapi.util.JavaUtil.Optional @@ -2710,6 +2709,21 @@ class StringFormatter private(val maybeSettings: Option[KnoraSettingsImpl] = Non } } + /** + * Checks whether an IRI already exists in the triplestore. + * + * @param iri the IRI to be checked. + * @param storeManager a reference to the store manager. + * @return `true` if the IRI already exists, `false` otherwise. + */ + def checkIriExists(iri: IRI, + storeManager: ActorRef)(implicit timeout: Timeout, executionContext: ExecutionContext): Future[Boolean] = { + for { + askString <- Future(queries.sparql.admin.txt.checkIriExists(iri).toString) + response <- (storeManager ? SparqlAskRequest(askString)).mapTo[SparqlAskResponse] + } yield response.result + } + /** * Attempts to create a new IRI that isn't already used in the triplestore. Will try up to [[MAX_IRI_ATTEMPTS]] * times, then throw an exception if an unused IRI could not be created. @@ -2724,10 +2738,9 @@ class StringFormatter private(val maybeSettings: Option[KnoraSettingsImpl] = Non val newIri = iriFun for { - askString <- Future(queries.sparql.admin.txt.checkIriExists(iri = newIri).toString) - response <- (storeManager ? SparqlAskRequest(askString)).mapTo[SparqlAskResponse] + iriExists <- checkIriExists(newIri, storeManager) - result <- if (!response.result) { + result <- if (!iriExists) { FastFuture.successful(newIri) } else if (attempts > 1) { log.warning("KnoraIdUtil.makeUnusedIri generated an IRI that already exists in the triplestore, retrying") diff --git a/webapi/src/main/scala/org/knora/webapi/util/jsonld/JsonLDUtil.scala b/webapi/src/main/scala/org/knora/webapi/util/jsonld/JsonLDUtil.scala index b868febdf7..ed294798ac 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/jsonld/JsonLDUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/jsonld/JsonLDUtil.scala @@ -19,6 +19,8 @@ package org.knora.webapi.util.jsonld +import java.util.UUID + import com.github.jsonldjava.core.{JsonLdOptions, JsonLdProcessor} import com.github.jsonldjava.utils.JsonUtils import org.knora.webapi._ @@ -423,16 +425,49 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * * @return a validated Knora data IRI. */ - def getIDAsKnoraDataIri: SmartIri = { + def requireIDAsKnoraDataIri: SmartIri = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + val iri = requireStringWithValidation(JsonLDConstants.ID, stringFormatter.toSmartIriWithErr) + + if (!iri.isKnoraDataIri) { + throw BadRequestException(s"Invalid Knora data IRI: $iri") + } + + iri + } + + /** + * Validates the optional `@id` of a JSON-LD object as a Knora data IRI. + * + * @return an optional validated Knora data IRI. + */ + def maybeIDAsKnoraDataIri: Option[SmartIri] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - val dataIri = requireStringWithValidation(JsonLDConstants.ID, stringFormatter.toSmartIriWithErr) + val maybeIri: Option[SmartIri] = maybeStringWithValidation(JsonLDConstants.ID, stringFormatter.toSmartIriWithErr) - if (!dataIri.isKnoraDataIri) { - throw BadRequestException(s"Invalid Knora data IRI: $dataIri") + maybeIri.foreach { + iri => + if (!iri.isKnoraDataIri) { + throw BadRequestException(s"Invalid Knora data IRI: $maybeIri") + } } - dataIri + maybeIri + } + + /** + * Validates the optional `uuid` of a JSON-LD object as a value uuid. + * + * @return an optional validated decoded UUID. + */ + def maybeUUID: Option[UUID] = { + implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + val maybeUUID: Option[UUID] = maybeStringWithValidation(OntologyConstants.KnoraApiV2Complex.ValueHasUUID, stringFormatter.validateBase64EncodedUuid) + + maybeUUID } /** @@ -440,7 +475,7 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * * @return a validated Knora type IRI. */ - def getTypeAsKnoraApiV2ComplexTypeIri: SmartIri = { + def requireTypeAsKnoraApiV2ComplexTypeIri: SmartIri = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance val typeIri = requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr) @@ -458,7 +493,7 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * * @return the property IRI and the value. */ - def getResourcePropertyApiV2ComplexValue: (SmartIri, JsonLDObject) = { + def requireResourcePropertyApiV2ComplexValue: (SmartIri, JsonLDObject) = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance val resourceProps: Map[IRI, JsonLDValue] = value - JsonLDConstants.ID - JsonLDConstants.TYPE @@ -621,19 +656,24 @@ case class JsonLDDocument(body: JsonLDObject, context: JsonLDObject = JsonLDObje def maybeBoolean(key: String): Option[Boolean] = body.maybeBoolean(key) /** - * A convenience function that calls `body.getIDAsKnoraDataIri`. + * A convenience function that calls `body.requireIDAsKnoraDataIri`. */ - def getIDAsKnoraDataIri: SmartIri = body.getIDAsKnoraDataIri + def requireIDAsKnoraDataIri: SmartIri = body.requireIDAsKnoraDataIri + + /** + * A convenience function that calls `body.maybeIDAsKnoraDataIri`. + */ + def maybeIDAsKnoraDataIri: Option[SmartIri] = body.maybeIDAsKnoraDataIri /** - * A convenience function that calls `body.getTypeAsKnoraApiV2ComplexTypeIri`. + * A convenience function that calls `body.requireTypeAsKnoraApiV2ComplexTypeIri`. */ - def getTypeAsKnoraTypeIri: SmartIri = body.getTypeAsKnoraApiV2ComplexTypeIri + def requireTypeAsKnoraTypeIri: SmartIri = body.requireTypeAsKnoraApiV2ComplexTypeIri /** - * A convenience function that calls `body.getResourcePropertyApiV2ComplexValue`. + * A convenience function that calls `body.requireResourcePropertyApiV2ComplexValue`. */ - def getResourcePropertyValue: (SmartIri, JsonLDObject) = body.getResourcePropertyApiV2ComplexValue + def requireResourcePropertyValue: (SmartIri, JsonLDObject) = body.requireResourcePropertyApiV2ComplexValue /** * Converts this JSON-LD object to its compacted Java representation. 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 d8b4cd1562..d988898ed0 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 @@ -22,6 +22,7 @@ package org.knora.webapi.e2e.v2 import java.io.File import java.net.URLEncoder import java.time.Instant +import java.util.UUID import akka.actor.ActorSystem import akka.http.scaladsl.model.headers.{Accept, BasicHttpCredentials} @@ -585,7 +586,7 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { } "create a resource with a custom creation date" in { - val creationDate: Instant = Instant.parse("2019-01-09T15:45:54.502951Z") + val creationDate: Instant = SharedTestDataADM.customResourceCreationDate val jsonLDEntity = SharedTestDataADM.createResourceWithCustomCreationDate(creationDate) val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) val response: HttpResponse = singleAwaitingRequest(request) @@ -603,6 +604,142 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { assert(savedCreationDate == creationDate) } + "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)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val resourceIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + assert(resourceIri == customIRI) + + } + + "create a resource with random Iri and a custom value Iri" in { + val customValueIRI: IRI = SharedTestDataADM.customValueIRI + val jsonLDEntity = SharedTestDataADM.createResourceWithCustomValueIRI(customValueIRI) + val request = Post(s"$baseApiUrl/v2/resources", 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 resourceIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + + // Request the newly created resource. + val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(resourceIri, "UTF-8")}") ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest, duration = settings.triplestoreUpdateTimeout) + val resourceGetResponseAsString = responseToString(resourceGetResponse) + + // Get the value from the response. + val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) + val valueIri: IRI = resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasBoolean"). + requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + assert(valueIri == customValueIRI) + + } + + "create a resource with random resource Iri and custom value UUIDs" in { + + val customValueUUID = SharedTestDataADM.customValueUUID + val jsonLDEntity = SharedTestDataADM.createResourceWithCustomValueUUID(customValueUUID = customValueUUID) + + val request = Post(s"$baseApiUrl/v2/resources", 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 resourceIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + + // Request the newly created resource. + val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(resourceIri, "UTF-8")}") ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest, duration = settings.triplestoreUpdateTimeout) + val resourceGetResponseAsString = responseToString(resourceGetResponse) + + // Get the value from the response. + val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) + val valueUUID = resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasBoolean").requireString(OntologyConstants.KnoraApiV2Complex.ValueHasUUID) + assert(valueUUID == customValueUUID) + + } + + "create a resource with random resource Iri and custom value creation date" in { + + val creationDate: Instant = SharedTestDataADM.customValueCreationDate + val jsonLDEntity = SharedTestDataADM.createResourceWithCustomValueCreationDate(creationDate = creationDate) + + val request = Post(s"$baseApiUrl/v2/resources", 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 resourceIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + + // Request the newly created resource. + val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(resourceIri, "UTF-8")}") ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest, duration = settings.triplestoreUpdateTimeout) + val resourceGetResponseAsString = responseToString(resourceGetResponse) + + // Get the value from the response. + val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) + val savedCreationDate: Instant = resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasBoolean").requireDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.ValueCreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) + assert(savedCreationDate == creationDate) + + } + + "create a resource with custom resource Iri, creation date, and a value with custom value Iri and UUID" in { + val customResourceIRI: IRI = SharedTestDataADM.customResourceIRI_resourceWithValues + val customCreationDate: Instant = Instant.parse("2019-01-09T15:45:54.502951Z") + val customValueIRI: IRI = SharedTestDataADM.customValueIRI_withResourceIriAndValueIRIAndValueUUID + val customValueUUID = SharedTestDataADM.customValueUUID + val jsonLDEntity = SharedTestDataADM.createResourceWithCustomResourceIriAndCreationDateAndValueWithCustomIRIAndUUID( + customResourceIRI = customResourceIRI, + customCreationDate = customCreationDate, + customValueIRI = customValueIRI, + customValueUUID = customValueUUID) + + val request = Post(s"$baseApiUrl/v2/resources", 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 resourceIri: IRI = responseJsonDoc.body.requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + assert(resourceIri == customResourceIRI) + + // Request the newly created resource. + val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(resourceIri, "UTF-8")}") ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest, duration = settings.triplestoreUpdateTimeout) + val resourceGetResponseAsString = responseToString(resourceGetResponse) + + // Get the value from the response. + val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) + val valueIri: IRI = resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasBoolean"). + requireStringWithValidation(JsonLDConstants.ID, stringFormatter.validateAndEscapeIri) + assert(valueIri == customValueIRI) + + val valueUUID = resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasBoolean").requireString(OntologyConstants.KnoraApiV2Complex.ValueHasUUID) + assert(valueUUID == customValueUUID) + + val savedCreationDate: Instant = responseJsonDoc.body.requireDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.CreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) + + assert(savedCreationDate == customCreationDate) + + // when no custom creation date is given to the value, it should have the same creation date as the resource + val savedValueCreationDate: Instant = resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasBoolean").requireDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.ValueCreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) + assert(savedValueCreationDate == customCreationDate) + + + } + "create a resource as another user" in { val jsonLDEntity = SharedTestDataADM.createResourceAsUser(SharedTestDataADM.anythingUser1) val request = Post(s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity)) ~> addCredentials(BasicHttpCredentials(SharedTestDataADM.anythingAdminUser.email, password)) 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 030d071348..da821b9da1 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 @@ -154,7 +154,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { userEmail = userEmail ) - val receivedResourceIri: IRI = resource.getIDAsKnoraDataIri.toString + val receivedResourceIri: IRI = resource.requireIDAsKnoraDataIri.toString if (receivedResourceIri != resourceIri) { throw AssertionException(s"Expected resource $resourceIri, received $receivedResourceIri") @@ -248,28 +248,100 @@ class ValuesRouteV2E2ESpec extends E2ESpec { savedIntValue should ===(intValue) } - "not create an integer value if @id is given" in { + "create an integer value with a custom valueIri" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri - val intValue: Int = 10 + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val intValue: Int = 30 + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + val customValueIri: IRI = "http://rdfh.ch/0001/a-customized-thing/values/int-with-valueIRI" + val jsonLdEntity = SharedTestDataADM.createIntValueWithCustomValueIriRequest( + resourceIri = resourceIri, + intValue = intValue, + valueIri = customValueIri + ) - val jsonLdEntity = - s"""{ - | "@id" : "$resourceIri", - | "@type" : "anything:Thing", - | "anything:hasInteger" : { - | "@id" : "${intValueIri.get}", - | "@type" : "knora-api:IntValue", - | "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#" - | } - |}""".stripMargin + 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) + assert(valueIri == customValueIri) + } + + "create an integer value with a custom UUID" 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 = 45 + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + val customValueUUID = "IN4R19yYR0ygi3K2VEHpUQ" + val jsonLdEntity = SharedTestDataADM.createIntValueWithCustomUUIDRequest( + resourceIri = resourceIri, + intValue = intValue, + valueUUID = customValueUUID + ) 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.BadRequest, response.toString) + + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val valueUUID = responseJsonDoc.body.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasUUID) + assert(valueUUID == customValueUUID) + } + + "create an integer value with a custom creation date" in { + val customCreationDate: Instant = Instant.parse("2020-06-04T11:36:54.502951Z") + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri + val intValue: Int = 25 + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + val jsonLdEntity = SharedTestDataADM.createIntValueWithCustomCreationDateRequest(resourceIri = resourceIri, intValue = intValue, creationDate = customCreationDate) + + 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 savedCreationDate: Instant = responseJsonDoc.body.requireDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.ValueCreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) + assert(savedCreationDate == customCreationDate) + } + + "create an integer value with custom Iri, UUID, and creation Date" 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 = 10 + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + val customValueIri: IRI = "http://rdfh.ch/0001/a-thing/values/int-with-IRI" + val customValueUUID = "IN4R19yYR0ygi3K2VEHpUQ" + val customCreationDate: Instant = Instant.parse("2020-06-04T12:58:54.502951Z") + val jsonLdEntity = SharedTestDataADM.createIntValueWithCustomIRIRequest( + resourceIri = resourceIri, + intValue = intValue, + valueIri = customValueIri, + valueUUID = customValueUUID, + valueCreationDate = customCreationDate + ) + + 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) + assert(valueIri == customValueIri) + val valueUUID = responseJsonDoc.body.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasUUID) + assert(valueUUID == customValueUUID) + val savedCreationDate: Instant = responseJsonDoc.body.requireDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.ValueCreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) + assert(savedCreationDate == customCreationDate) } "not create an integer value if the simple schema is submitted" in { @@ -1890,6 +1962,39 @@ class ValuesRouteV2E2ESpec extends E2ESpec { savedTargetIri should ===(SharedTestDataADM.TestDing.iri) } + "create a link between two resources with a custom link value Iri, UUID, creationDate" in { + val resourceIri: IRI = SharedTestDataADM.AThing.iri + val targetResourceIri: IRI = "http://rdfh.ch/0001/CNhWoNGGT7iWOrIwxsEqvA" + val customValueIri: IRI = "http://rdfh.ch/0001/a-thing/values/link-Value-With-IRI" + val customValueUUID = "IN4R19yYR0ygi3K2VEHpUQ" + val customCreationDate: Instant = Instant.parse("2020-06-04T11:36:54.502951Z") + val maybeResourceLastModDate: Option[Instant] = getResourceLastModificationDate(resourceIri, anythingUserEmail) + + val jsonLdEntity = SharedTestDataADM.createLinkValueWithCustomIriRequest( + resourceIri = resourceIri, + targetResourceIri = targetResourceIri, + customValueIri = customValueIri, + customValueUUID = customValueUUID, + customValueCreationDate = customCreationDate + ) + + 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) + assert(valueIri == customValueIri) + val valueUUID: IRI = responseJsonDoc.body.requireString(OntologyConstants.KnoraApiV2Complex.ValueHasUUID) + assert(valueUUID == customValueUUID) + val savedCreationDate: Instant = responseJsonDoc.body.requireDatatypeValueInObject( + key = OntologyConstants.KnoraApiV2Complex.ValueCreationDate, + expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri, + validationFun = stringFormatter.xsdDateTimeStampToInstant + ) + assert(savedCreationDate == customCreationDate) + } + "update an integer value" in { val resourceIri: IRI = SharedTestDataADM.AThing.iri val propertyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger".toSmartIri