Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(OntologyResponderV2): Fix check when updating ontology label and comment (DSP-1390) #1826

Merged
merged 6 commits into from Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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