Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix(OntologyResponderV2): Fix check when updating ontology label and …
…comment (DSP-1390) (#1826)
  • Loading branch information
Benjamin Geer committed Mar 11, 2021
1 parent e392bf1 commit 26cce48
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 3 deletions.
12 changes: 12 additions & 0 deletions docs/03-apis/api-v2/ontology-information.md
Expand Up @@ -1025,6 +1025,18 @@ The request body can also contain a new label and a new comment for the ontology
A successful response will be a JSON-LD document providing only the
ontology's metadata.

### Deleting an Ontology's comment

```
HTTP DELETE to http://host/v2/ontologies/comment/ONTOLOGY_IRI?lastModificationDate=ONTOLOGY_LAST_MODIFICATION_DATE
```

The ontology IRI and the ontology's last modification date must be
URL-encoded.

A successful response will be a JSON-LD document containing the ontology's
updated metadata.

### Deleting an Ontology

An ontology can be deleted only if it is not used in data.
Expand Down
Expand Up @@ -994,6 +994,21 @@ object ChangeOntologyMetadataRequestV2 extends KnoraJsonLDRequestReaderV2[Change
}
}

/**
* Deletes the comment from an ontology. A successful response will be a [[ReadOntologyMetadataV2]].
*
* @param ontologyIri the external ontology IRI.
* @param lastModificationDate the ontology's last modification date, returned in a previous operation.
* @param apiRequestID the ID of the API request.
* @param requestingUser the user making the request.
*/
case class DeleteOntologyCommentRequestV2(ontologyIri: SmartIri,
lastModificationDate: Instant,
apiRequestID: UUID,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM)
extends OntologiesResponderRequestV2

/**
* Requests all available information about a list of ontology entities (classes and/or properties). A successful response will be an
* [[EntityInfoGetResponseV2]].
Expand Down
Expand Up @@ -136,6 +136,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
case createOntologyRequest: CreateOntologyRequestV2 => createOntology(createOntologyRequest)
case changeOntologyMetadataRequest: ChangeOntologyMetadataRequestV2 =>
changeOntologyMetadata(changeOntologyMetadataRequest)
case deleteOntologyCommentRequest: DeleteOntologyCommentRequestV2 =>
deleteOntologyComment(deleteOntologyCommentRequest)
case createClassRequest: CreateClassRequestV2 => createClass(createClassRequest)
case changeClassLabelsOrCommentsRequest: ChangeClassLabelsOrCommentsRequestV2 =>
changeClassLabelsOrComments(changeClassLabelsOrCommentsRequest)
Expand Down Expand Up @@ -2140,6 +2142,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
ontologyIri = internalOntologyIri,
newLabel = changeOntologyMetadataRequest.label,
hasOldComment = ontologyHasComment,
deleteOldComment = ontologyHasComment && changeOntologyMetadataRequest.comment.nonEmpty,
newComment = changeOntologyMetadataRequest.comment,
lastModificationDate = changeOntologyMetadataRequest.lastModificationDate,
currentTime = currentTime
Expand All @@ -2159,11 +2162,20 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
changeOntologyMetadataRequest.label
}

// Is there any new comment given?
comment = if (changeOntologyMetadataRequest.comment.isEmpty) {
// No. Consider the old comment for checking the update.
oldMetadata.comment
} else {
// Yes. Consider the new comment for checking the update.
changeOntologyMetadataRequest.comment
}

unescapedNewMetadata = OntologyMetadataV2(
ontologyIri = internalOntologyIri,
projectIri = Some(projectIri),
label = label,
comment = changeOntologyMetadataRequest.comment,
comment = comment,
lastModificationDate = Some(currentTime)
).unescape

Expand Down Expand Up @@ -2206,6 +2218,99 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
} yield taskResult
}

def deleteOntologyComment(
deleteOntologyCommentRequestV2: DeleteOntologyCommentRequestV2): Future[ReadOntologyMetadataV2] = {
def makeTaskFuture(internalOntologyIri: SmartIri): Future[ReadOntologyMetadataV2] = {
for {
cacheData <- getCacheData

// Check that the user has permission to update the ontology.
projectIri <- checkPermissionsForOntologyUpdate(
internalOntologyIri = internalOntologyIri,
requestingUser = deleteOntologyCommentRequestV2.requestingUser
)

// Check that the ontology exists and has not been updated by another user since the client last read its metadata.
_ <- checkOntologyLastModificationDateBeforeUpdate(
internalOntologyIri = internalOntologyIri,
expectedLastModificationDate = deleteOntologyCommentRequestV2.lastModificationDate,
featureFactoryConfig = deleteOntologyCommentRequestV2.featureFactoryConfig
)

// get the metadata of the ontology.
oldMetadata: OntologyMetadataV2 = cacheData.ontologies(internalOntologyIri).ontologyMetadata
// Was there a comment in the ontology metadata?
ontologyHasComment: Boolean = oldMetadata.comment.nonEmpty

// Update the metadata.

currentTime: Instant = Instant.now

updateSparql = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.changeOntologyMetadata(
triplestore = settings.triplestoreType,
ontologyNamedGraphIri = internalOntologyIri,
ontologyIri = internalOntologyIri,
newLabel = None,
hasOldComment = ontologyHasComment,
deleteOldComment = true,
newComment = None,
lastModificationDate = deleteOntologyCommentRequestV2.lastModificationDate,
currentTime = currentTime
)
.toString()

_ <- (storeManager ? SparqlUpdateRequest(updateSparql)).mapTo[SparqlUpdateResponse]

// Check that the update was successful.

unescapedNewMetadata = OntologyMetadataV2(
ontologyIri = internalOntologyIri,
projectIri = Some(projectIri),
label = oldMetadata.label,
comment = None,
lastModificationDate = Some(currentTime)
).unescape

maybeLoadedOntologyMetadata: Option[OntologyMetadataV2] <- loadOntologyMetadata(
internalOntologyIri = internalOntologyIri,
featureFactoryConfig = deleteOntologyCommentRequestV2.featureFactoryConfig
)

_ = maybeLoadedOntologyMetadata match {
case Some(loadedOntologyMetadata) =>
if (loadedOntologyMetadata != unescapedNewMetadata) {
throw UpdateNotPerformedException()
}

case None => throw UpdateNotPerformedException()
}

// Update the ontology cache with the unescaped metadata.

_ = storeCacheData(
cacheData.copy(
ontologies = cacheData.ontologies + (internalOntologyIri -> cacheData
.ontologies(internalOntologyIri)
.copy(ontologyMetadata = unescapedNewMetadata))
))

} yield ReadOntologyMetadataV2(ontologies = Set(unescapedNewMetadata))
}

for {
_ <- checkExternalOntologyIriForUpdate(deleteOntologyCommentRequestV2.ontologyIri)
internalOntologyIri = deleteOntologyCommentRequestV2.ontologyIri.toOntologySchema(InternalSchema)

// Do the remaining pre-update checks and the update while holding a global ontology cache lock.
taskResult <- IriLocker.runWithIriLock(
apiRequestID = deleteOntologyCommentRequestV2.apiRequestID,
iri = ONTOLOGY_CACHE_LOCK_IRI,
task = () => makeTaskFuture(internalOntologyIri = internalOntologyIri)
)
} yield taskResult
}

/**
* Creates a class in an existing ontology.
*
Expand Down
Expand Up @@ -63,6 +63,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData)
replaceCardinalities(featureFactoryConfig) ~
getClasses(featureFactoryConfig) ~
deleteClass(featureFactoryConfig) ~
deleteOntologyComment(featureFactoryConfig) ~
createProperty(featureFactoryConfig) ~
updateProperty(featureFactoryConfig) ~
getProperties(featureFactoryConfig) ~
Expand Down Expand Up @@ -560,6 +561,54 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData)
}
}

private def deleteOntologyComment(featureFactoryConfig: FeatureFactoryConfig): Route =
path(OntologiesBasePath / "comment" / Segment) { ontologyIriStr: IRI =>
delete { requestContext =>
{

val ontologyIri = ontologyIriStr.toSmartIri

if (!ontologyIri.getOntologySchema.contains(ApiV2Complex)) {
throw BadRequestException(s"Invalid class IRI for request: $ontologyIriStr")
}

val lastModificationDateStr = requestContext.request.uri
.query()
.toMap
.getOrElse(LAST_MODIFICATION_DATE, throw BadRequestException(s"Missing parameter: $LAST_MODIFICATION_DATE"))

val lastModificationDate = stringFormatter.xsdDateTimeStampToInstant(
lastModificationDateStr,
throw BadRequestException(s"Invalid timestamp: $lastModificationDateStr"))

val requestMessageFuture: Future[DeleteOntologyCommentRequestV2] = for {
requestingUser <- getUserADM(
requestContext = requestContext,
featureFactoryConfig = featureFactoryConfig
)
} yield
DeleteOntologyCommentRequestV2(
ontologyIri = ontologyIri,
lastModificationDate = lastModificationDate,
apiRequestID = UUID.randomUUID,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)

RouteUtilV2.runRdfRouteWithFuture(
requestMessageF = requestMessageFuture,
requestContext = requestContext,
featureFactoryConfig = featureFactoryConfig,
settings = settings,
responderManager = responderManager,
log = log,
targetSchema = ApiV2Complex,
schemaOptions = RouteUtilV2.getSchemaOptions(requestContext)
)
}
}
}

private def createProperty(featureFactoryConfig: FeatureFactoryConfig): Route =
path(OntologiesBasePath / "properties") {
post {
Expand Down
Expand Up @@ -27,6 +27,8 @@
* @param ontologyNamedGraphIri the IRI of the named graph where the ontology should be stored.
* @param ontologyIri the IRI of the ontology to be created.
* @param newLabel the ontology's new label.
* @param hasOldComment true if the ontology has a comment.
* @param deleteOldComment true if the existing comment should be deleted.
* @param newComment the ontology's new comment.
* @param lastModificationDate the xsd:dateTimeStamp that was attached to the ontology when it was last modified.
* @param currentTime an xsd:dateTimeStamp that will be attached to the ontology.
Expand All @@ -36,6 +38,7 @@
ontologyIri: SmartIri,
newLabel: Option[String],
hasOldComment: Boolean,
deleteOldComment: Boolean,
newComment: Option[String],
lastModificationDate: Instant,
currentTime: Instant)
Expand All @@ -51,7 +54,7 @@ DELETE {
@if(newLabel.nonEmpty) {
?ontology rdfs:label ?oldLabel .
}
@if(hasOldComment && newComment.nonEmpty) {
@if(hasOldComment && (deleteOldComment || newComment.nonEmpty)) {
?ontology rdfs:comment ?oldComment .
}
?ontology knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime .
Expand Down Expand Up @@ -81,7 +84,7 @@ WHERE {
@if(newLabel.nonEmpty) {
rdfs:label ?oldLabel ;
}
@if(hasOldComment && newComment.nonEmpty) {
@if(hasOldComment) {
rdfs:comment ?oldComment ;
}
knora-base:lastModificationDate "@lastModificationDate"^^xsd:dateTime .
Expand Down
Expand Up @@ -561,6 +561,31 @@ class OntologyV2R2RSpec extends R2RSpec {
}
}

"delete the comment from 'foo'" in {
val fooIriEncoded = URLEncoder.encode(fooIri.get, "UTF-8")
val lastModificationDate = URLEncoder.encode(fooLastModDate.toString, "UTF-8")

Delete(s"/v2/ontologies/comment/$fooIriEncoded?lastModificationDate=$lastModificationDate") ~> addCredentials(
BasicHttpCredentials(anythingUsername, password)) ~> ontologiesPath ~> check {
assert(status == StatusCodes.OK, response.toString)
val responseJsonDoc = responseToJsonLDDocument(response)
val metadata = responseJsonDoc.body
val ontologyIri = metadata.value("@id").asInstanceOf[JsonLDString].value
assert(ontologyIri == fooIri.get)
assert(metadata.value(OntologyConstants.Rdfs.Label) == JsonLDString("The modified foo ontology"))
assert(!metadata.value.contains(OntologyConstants.Rdfs.Comment))

val lastModDate = metadata.requireDatatypeValueInObject(
key = OntologyConstants.KnoraApiV2Complex.LastModificationDate,
expectedDatatype = OntologyConstants.Xsd.DateTimeStamp.toSmartIri,
validationFun = stringFormatter.xsdDateTimeStampToInstant
)

assert(lastModDate.isAfter(fooLastModDate))
fooLastModDate = lastModDate
}
}

"delete the 'foo' ontology" in {
val fooIriEncoded = URLEncoder.encode(fooIri.get, "UTF-8")
val lastModificationDate = URLEncoder.encode(fooLastModDate.toString, "UTF-8")
Expand Down
Expand Up @@ -174,6 +174,79 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
fooLastModDate = newFooLastModDate
}

"change both the label and the comment of the 'foo' ontology" in {
val aLabel = "a changed label"
val aComment = "a changed comment"

responderManager ! ChangeOntologyMetadataRequestV2(
ontologyIri = fooIri.get.toSmartIri.toOntologySchema(ApiV2Complex),
label = Some(aLabel),
comment = Some(aComment),
lastModificationDate = fooLastModDate,
apiRequestID = UUID.randomUUID,
featureFactoryConfig = defaultFeatureFactoryConfig,
requestingUser = imagesUser
)

val response = expectMsgType[ReadOntologyMetadataV2](timeout)
assert(response.ontologies.size == 1)
val metadata = response.ontologies.head
assert(metadata.ontologyIri == fooIri.get.toSmartIri)
assert(metadata.label.contains(aLabel))
assert(metadata.comment.contains(aComment))
val newFooLastModDate = metadata.lastModificationDate.getOrElse(
throw AssertionException(s"${metadata.ontologyIri} has no last modification date"))
assert(newFooLastModDate.isAfter(fooLastModDate))
fooLastModDate = newFooLastModDate
}

"change the label of 'foo' again" in {
val newLabel = "a label changed again"

responderManager ! ChangeOntologyMetadataRequestV2(
ontologyIri = fooIri.get.toSmartIri.toOntologySchema(ApiV2Complex),
label = Some(newLabel),
lastModificationDate = fooLastModDate,
apiRequestID = UUID.randomUUID,
featureFactoryConfig = defaultFeatureFactoryConfig,
requestingUser = imagesUser
)

val response = expectMsgType[ReadOntologyMetadataV2](timeout)
assert(response.ontologies.size == 1)
val metadata = response.ontologies.head
assert(metadata.ontologyIri == fooIri.get.toSmartIri)
assert(metadata.label.contains(newLabel))
assert(metadata.comment.contains("a changed comment"))
val newFooLastModDate = metadata.lastModificationDate.getOrElse(
throw AssertionException(s"${metadata.ontologyIri} has no last modification date"))
assert(newFooLastModDate.isAfter(fooLastModDate))
fooLastModDate = newFooLastModDate
}

"delete the comment from 'foo'" in {
val newLabel = "a label changed again"

responderManager ! DeleteOntologyCommentRequestV2(
ontologyIri = fooIri.get.toSmartIri.toOntologySchema(ApiV2Complex),
lastModificationDate = fooLastModDate,
apiRequestID = UUID.randomUUID,
featureFactoryConfig = defaultFeatureFactoryConfig,
requestingUser = imagesUser
)

val response = expectMsgType[ReadOntologyMetadataV2](timeout)
assert(response.ontologies.size == 1)
val metadata = response.ontologies.head
assert(metadata.ontologyIri == fooIri.get.toSmartIri)
assert(metadata.label.contains("a label changed again"))
assert(metadata.comment.isEmpty)
val newFooLastModDate = metadata.lastModificationDate.getOrElse(
throw AssertionException(s"${metadata.ontologyIri} has no last modification date"))
assert(newFooLastModDate.isAfter(fooLastModDate))
fooLastModDate = newFooLastModDate
}

"create an empty ontology called 'bar' with a comment" in {
responderManager ! CreateOntologyRequestV2(
ontologyName = "bar",
Expand Down

0 comments on commit 26cce48

Please sign in to comment.