Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(api-v2): Accept custom new value IRI when updating value (#1698)
  • Loading branch information
Benjamin Geer committed Sep 8, 2020
1 parent a9e72d8 commit 4d8f867
Show file tree
Hide file tree
Showing 11 changed files with 446 additions and 135 deletions.
16 changes: 13 additions & 3 deletions docs/03-apis/api-v2/editing-resources.md
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
30 changes: 28 additions & 2 deletions docs/03-apis/api-v2/editing-values.md
Expand Up @@ -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",
Expand Down Expand Up @@ -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.

Expand Down
Expand Up @@ -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"
Expand Down
Expand Up @@ -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
}
}
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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
)
}
}
Expand Down Expand Up @@ -862,44 +895,50 @@ 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,
propertyIri: SmartIri,
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,
propertyIri: SmartIri,
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.
Expand Down

0 comments on commit 4d8f867

Please sign in to comment.