Skip to content

Commit eca9206

Browse files
author
irinaschubert
authored
feat(ontology): allow deleting comments of classes (DEV-804) (#2048)
* add method and test to delete property comment * add route to delete property comment * allow deleting comments for properties * improve code * fix failing test * add more tests * wip * add test for link property * add route * add e2e test * update documentation * add tests * add route to delete comment of class
1 parent 985c5fd commit eca9206

8 files changed

Lines changed: 546 additions & 36 deletions

File tree

‎docs/03-apis/api-v2/ontology-information.md‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,7 @@ HTTP POST to http://host/v2/ontologies/classes
11971197
}
11981198
```
11991199

1200-
Values for `rdfs:label` and `rdfs:comment` must be submitted in at least
1200+
Values for `rdfs:label` must be submitted in at least
12011201
one language, either as an object or as an array of objects.
12021202

12031203
At least one base class must be provided, which can be
@@ -1262,7 +1262,7 @@ to the supported combinations given in
12621262
`OWL_CARDINALITY_VALUE` is shown here in quotes, but it should be an
12631263
unquoted integer.)
12641264

1265-
Values for `rdfs:label` and `rdfs:comment` must be submitted in at least
1265+
Values for `rdfs:label` must be submitted in at least
12661266
one language, either as an object or as an array of objects.
12671267

12681268
At least one base class must be provided.
@@ -1353,6 +1353,20 @@ Values for `rdfs:comment` must be submitted in at least one language,
13531353
either as an object or as an array of objects. The submitted comments
13541354
will replace the existing ones.
13551355

1356+
### Deleting the Comments of a Class
1357+
1358+
This operation is permitted even if the class is used in data.
1359+
1360+
```
1361+
HTTP DELETE to http://host/v2/ontologies/classes/comment/CLASS_IRI?lastModificationDate=ONTOLOGY_LAST_MODIFICATION_DATE
1362+
```
1363+
1364+
The class IRI and the ontology's last modification date must be URL-encoded.
1365+
1366+
All values i.e. all languages for `rdfs:comment` are deleted.
1367+
1368+
A successful response will be a JSON-LD document providing the class definition.
1369+
13561370
### Creating a Property
13571371

13581372
```
@@ -1404,7 +1418,7 @@ HTTP POST to http://host/v2/ontologies/properties
14041418
}
14051419
```
14061420

1407-
Values for `rdfs:label` and `rdfs:comment` must be submitted in at least
1421+
Values for `rdfs:label` must be submitted in at least
14081422
one language, either as an object or as an array of objects.
14091423

14101424
At least one base property must be provided, which can be

‎test_data/ontologies/freetest-onto.ttl‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,38 @@
312312
knora-base:TextValue ;
313313
salsah-gui:guiElement salsah-gui:Richtext .
314314

315+
:BookWithComment
316+
rdf:type owl:Class ;
317+
rdfs:subClassOf knora-base:Resource,
318+
[ rdf:type owl:Restriction ;
319+
owl:onProperty :hasName ;
320+
owl:minCardinality "0"^^xsd:nonNegativeInteger ;
321+
salsah-gui:guiOrder "1"^^xsd:nonNegativeInteger ] ;
322+
rdfs:label "Buch mit Kommentar"@de,
323+
"Book with comment"@en ;
324+
rdfs:comment """A comment for book"""@en .
325+
326+
:BookWithoutComment
327+
rdf:type owl:Class ;
328+
rdfs:subClassOf knora-base:Resource,
329+
[ rdf:type owl:Restriction ;
330+
owl:onProperty :hasName ;
331+
owl:minCardinality "0"^^xsd:nonNegativeInteger ;
332+
salsah-gui:guiOrder "1"^^xsd:nonNegativeInteger ] ;
333+
rdfs:label "Buch ohne Kommentar"@de,
334+
"Book without comment"@en .
335+
336+
:BookWithComment2
337+
rdf:type owl:Class ;
338+
rdfs:subClassOf knora-base:Resource,
339+
[ rdf:type owl:Restriction ;
340+
owl:onProperty :hasName ;
341+
owl:minCardinality "0"^^xsd:nonNegativeInteger ;
342+
salsah-gui:guiOrder "1"^^xsd:nonNegativeInteger ] ;
343+
rdfs:label "Buch 2 mit Kommentar"@de,
344+
"Book 2 with comment"@en ;
345+
rdfs:comment """A comment for book"""@en .
346+
315347
:hasFoafName
316348
rdf:type owl:ObjectProperty ;
317349
rdfs:subPropertyOf knora-base:hasValue,
@@ -348,8 +380,8 @@
348380
:hasPropertyWithComment2
349381
rdf:type owl:ObjectProperty ;
350382
rdfs:subPropertyOf knora-base:hasValue ;
351-
rdfs:label "Property mit einem Kommentar"@de,
352-
"Property with a comment"@en ;
383+
rdfs:label "Property mit einem Kommentar 2"@de,
384+
"Property with a comment 2"@en ;
353385
rdfs:comment "Dies ist der Kommentar"@de,
354386
"This is the comment"@en ;
355387
knora-base:objectClassConstraint knora-base:TextValue ;

‎webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,80 @@ object ChangeClassLabelsOrCommentsRequestV2 extends KnoraJsonLDRequestReaderV2[C
13091309
}
13101310
}
13111311

1312+
/**
1313+
* Deletes the comment from a class. A successful response will be a [[ReadOntologyV2]].
1314+
*
1315+
* @param classIri the IRI of the class.
1316+
* @param lastModificationDate the ontology's last modification date
1317+
* @param apiRequestID the ID of the API request.
1318+
* @param requestingUser the user making the request.
1319+
*/
1320+
case class DeleteClassCommentRequestV2(
1321+
classIri: SmartIri,
1322+
lastModificationDate: Instant,
1323+
apiRequestID: UUID,
1324+
featureFactoryConfig: FeatureFactoryConfig,
1325+
requestingUser: UserADM
1326+
) extends OntologiesResponderRequestV2
1327+
1328+
/**
1329+
* Constructs instances of [[DeleteClassCommentRequestV2]] based on JSON-LD input.
1330+
*/
1331+
object DeleteClassCommentRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteClassCommentRequestV2] {
1332+
1333+
/**
1334+
* Converts a JSON-LD request to a [[DeleteClassCommentRequestV2]].
1335+
*
1336+
* @param jsonLDDocument the JSON-LD input.
1337+
* @param apiRequestID the UUID of the API request.
1338+
* @param requestingUser the user making the request.
1339+
* @param responderManager a reference to the responder manager.
1340+
* @param storeManager a reference to the store manager.
1341+
* @param featureFactoryConfig the feature factory configuration.
1342+
* @param settings the application settings.
1343+
* @param log a logging adapter.
1344+
* @return a [[DeleteClassCommentRequestV2]] representing the input.
1345+
*/
1346+
override def fromJsonLD(
1347+
jsonLDDocument: JsonLDDocument,
1348+
apiRequestID: UUID,
1349+
requestingUser: UserADM,
1350+
responderManager: ActorRef,
1351+
storeManager: ActorRef,
1352+
featureFactoryConfig: FeatureFactoryConfig,
1353+
settings: KnoraSettingsImpl,
1354+
log: LoggingAdapter
1355+
)(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DeleteClassCommentRequestV2] =
1356+
Future {
1357+
fromJsonLDSync(
1358+
jsonLDDocument = jsonLDDocument,
1359+
apiRequestID = apiRequestID,
1360+
featureFactoryConfig = featureFactoryConfig,
1361+
requestingUser = requestingUser
1362+
)
1363+
}
1364+
1365+
private def fromJsonLDSync(
1366+
jsonLDDocument: JsonLDDocument,
1367+
apiRequestID: UUID,
1368+
featureFactoryConfig: FeatureFactoryConfig,
1369+
requestingUser: UserADM
1370+
): DeleteClassCommentRequestV2 = {
1371+
val inputOntologyV2: InputOntologyV2 = InputOntologyV2.fromJsonLD(jsonLDDocument)
1372+
val classUpdateInfo: ClassUpdateInfo = OntologyUpdateHelper.getClassDef(inputOntologyV2)
1373+
val classInfoContent: ClassInfoContentV2 = classUpdateInfo.classInfoContent
1374+
val lastModificationDate: Instant = classUpdateInfo.lastModificationDate
1375+
1376+
DeleteClassCommentRequestV2(
1377+
classIri = classInfoContent.classIri,
1378+
lastModificationDate = lastModificationDate,
1379+
apiRequestID = apiRequestID,
1380+
featureFactoryConfig = featureFactoryConfig,
1381+
requestingUser = requestingUser
1382+
)
1383+
}
1384+
}
1385+
13121386
case class ChangeGuiOrderRequestV2(
13131387
classInfoContent: ClassInfoContentV2,
13141388
lastModificationDate: Instant,

‎webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
114114
changePropertyLabelsOrComments(changePropertyLabelsOrCommentsRequest)
115115
case deletePropertyCommentRequest: DeletePropertyCommentRequestV2 =>
116116
deletePropertyComment(deletePropertyCommentRequest)
117+
case deleteClassCommentRequest: DeleteClassCommentRequestV2 =>
118+
deleteClassComment(deleteClassCommentRequest)
117119
case changePropertyGuiElementRequest: ChangePropertyGuiElementRequest =>
118120
changePropertyGuiElement(changePropertyGuiElementRequest)
119121
case canDeletePropertyRequest: CanDeletePropertyRequestV2 => canDeleteProperty(canDeletePropertyRequest)
@@ -3497,4 +3499,164 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
34973499
} yield taskResult
34983500
}
34993501

3502+
/**
3503+
* Delete the `rdfs:comment` in a class definition.
3504+
*
3505+
* @param deleteClassCommentRequestV2 the request to delete the class' comment
3506+
* @return a [[ReadOntologyV2]] containing the modified class definition.
3507+
*/
3508+
private def deleteClassComment(
3509+
deleteClassCommentRequest: DeleteClassCommentRequestV2
3510+
): Future[ReadOntologyV2] = {
3511+
def makeTaskFuture(
3512+
cacheData: Cache.OntologyCacheData,
3513+
internalClassIri: SmartIri,
3514+
internalOntologyIri: SmartIri,
3515+
ontology: ReadOntologyV2,
3516+
classToUpdate: ReadClassInfoV2
3517+
): Future[ReadOntologyV2] =
3518+
for {
3519+
3520+
// Check that the ontology exists and has not been updated by another user since the client last read its metadata.
3521+
_ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate(
3522+
settings,
3523+
storeManager,
3524+
internalOntologyIri = internalOntologyIri,
3525+
expectedLastModificationDate = deleteClassCommentRequest.lastModificationDate,
3526+
featureFactoryConfig = deleteClassCommentRequest.featureFactoryConfig
3527+
)
3528+
3529+
currentTime: Instant = Instant.now
3530+
3531+
// Delete the comment
3532+
updateSparql: String = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
3533+
.deleteClassComment(
3534+
ontologyNamedGraphIri = internalOntologyIri,
3535+
ontologyIri = internalOntologyIri,
3536+
classIri = internalClassIri,
3537+
lastModificationDate = deleteClassCommentRequest.lastModificationDate,
3538+
currentTime = currentTime
3539+
)
3540+
.toString()
3541+
3542+
_ <- (storeManager ? SparqlUpdateRequest(updateSparql)).mapTo[SparqlUpdateResponse]
3543+
3544+
// Check that the ontology's last modification date was updated.
3545+
_ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate(
3546+
settings = settings,
3547+
storeManager = storeManager,
3548+
internalOntologyIri = internalOntologyIri,
3549+
expectedLastModificationDate = currentTime,
3550+
featureFactoryConfig = deleteClassCommentRequest.featureFactoryConfig
3551+
)
3552+
3553+
// Check that the update was successful.
3554+
loadedClassDef: ClassInfoContentV2 <- OntologyHelpers.loadClassDefinition(
3555+
settings,
3556+
storeManager,
3557+
classIri = internalClassIri,
3558+
featureFactoryConfig = deleteClassCommentRequest.featureFactoryConfig
3559+
)
3560+
3561+
classDefWithoutComment: ClassInfoContentV2 =
3562+
classToUpdate.entityInfoContent.copy(
3563+
predicates = classToUpdate.entityInfoContent.predicates.-(
3564+
OntologyConstants.Rdfs.Comment.toSmartIri
3565+
) // the "-" deletes the entry with the comment
3566+
)
3567+
3568+
_ = if (loadedClassDef != classDefWithoutComment) {
3569+
throw InconsistentRepositoryDataException(
3570+
s"Attempted to save class definition $classDefWithoutComment, but $loadedClassDef was saved"
3571+
)
3572+
}
3573+
3574+
// Update the ontology cache using the new class definition.
3575+
newReadClassInfo: ReadClassInfoV2 = ReadClassInfoV2(
3576+
entityInfoContent = loadedClassDef,
3577+
allBaseClasses = classToUpdate.allBaseClasses
3578+
)
3579+
3580+
updatedOntologyMetadata: OntologyMetadataV2 = ontology.ontologyMetadata.copy(
3581+
lastModificationDate = Some(currentTime)
3582+
)
3583+
3584+
updatedOntology: ReadOntologyV2 =
3585+
ontology.copy(
3586+
ontologyMetadata = updatedOntologyMetadata,
3587+
classes = ontology.classes + (internalClassIri -> newReadClassInfo)
3588+
)
3589+
3590+
_ = Cache.storeCacheData(
3591+
cacheData.copy(
3592+
ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology)
3593+
)
3594+
)
3595+
3596+
// Read the data back from the cache.
3597+
3598+
response: ReadOntologyV2 <- getClassDefinitionsFromOntologyV2(
3599+
classIris = Set(internalClassIri),
3600+
allLanguages = true,
3601+
requestingUser = deleteClassCommentRequest.requestingUser
3602+
)
3603+
3604+
} yield response
3605+
3606+
for {
3607+
requestingUser: UserADM <- FastFuture.successful(deleteClassCommentRequest.requestingUser)
3608+
3609+
externalClassIri: SmartIri = deleteClassCommentRequest.classIri
3610+
externalOntologyIri: SmartIri = externalClassIri.getOntologyFromEntity
3611+
3612+
_ <- OntologyHelpers.checkOntologyAndEntityIrisForUpdate(
3613+
externalOntologyIri = externalOntologyIri,
3614+
externalEntityIri = externalClassIri,
3615+
requestingUser = requestingUser
3616+
)
3617+
3618+
internalClassIri: SmartIri = externalClassIri.toOntologySchema(InternalSchema)
3619+
internalOntologyIri: SmartIri = externalOntologyIri.toOntologySchema(InternalSchema)
3620+
3621+
cacheData: Cache.OntologyCacheData <- Cache.getCacheData
3622+
3623+
ontology: ReadOntologyV2 = cacheData.ontologies(internalOntologyIri)
3624+
3625+
classToUpdate: ReadClassInfoV2 =
3626+
ontology.classes.getOrElse(
3627+
internalClassIri,
3628+
throw NotFoundException(s"Class ${deleteClassCommentRequest.classIri} not found")
3629+
)
3630+
3631+
hasComment: Boolean = classToUpdate.entityInfoContent.predicates.contains(
3632+
OntologyConstants.Rdfs.Comment.toSmartIri
3633+
)
3634+
3635+
taskResult: ReadOntologyV2 <-
3636+
if (hasComment) for {
3637+
// Do the remaining pre-update checks and the update while holding a global ontology cache lock.
3638+
taskResult: ReadOntologyV2 <- IriLocker.runWithIriLock(
3639+
apiRequestID = deleteClassCommentRequest.apiRequestID,
3640+
iri = ONTOLOGY_CACHE_LOCK_IRI,
3641+
task = () =>
3642+
makeTaskFuture(
3643+
cacheData = cacheData,
3644+
internalClassIri = internalClassIri,
3645+
internalOntologyIri = internalOntologyIri,
3646+
ontology = ontology,
3647+
classToUpdate = classToUpdate
3648+
)
3649+
)
3650+
} yield taskResult
3651+
else {
3652+
// not change anything if class has no comment
3653+
getClassDefinitionsFromOntologyV2(
3654+
classIris = Set(internalClassIri),
3655+
allLanguages = true,
3656+
requestingUser = deleteClassCommentRequest.requestingUser
3657+
)
3658+
}
3659+
} yield taskResult
3660+
}
3661+
35003662
}

0 commit comments

Comments
 (0)