Skip to content

Commit

Permalink
feat(ontology): allow deleting comments of classes (DEV-804) (#2048)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
irinaschubert committed Apr 26, 2022
1 parent 985c5fd commit eca9206
Show file tree
Hide file tree
Showing 8 changed files with 546 additions and 36 deletions.
20 changes: 17 additions & 3 deletions docs/03-apis/api-v2/ontology-information.md
Expand Up @@ -1197,7 +1197,7 @@ HTTP POST to http://host/v2/ontologies/classes
}
```

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

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

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

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

### Deleting the Comments of a Class

This operation is permitted even if the class is used in data.

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

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

All values i.e. all languages for `rdfs:comment` are deleted.

A successful response will be a JSON-LD document providing the class definition.

### Creating a Property

```
Expand Down Expand Up @@ -1404,7 +1418,7 @@ HTTP POST to http://host/v2/ontologies/properties
}
```

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

At least one base property must be provided, which can be
Expand Down
36 changes: 34 additions & 2 deletions test_data/ontologies/freetest-onto.ttl
Expand Up @@ -312,6 +312,38 @@
knora-base:TextValue ;
salsah-gui:guiElement salsah-gui:Richtext .

:BookWithComment
rdf:type owl:Class ;
rdfs:subClassOf knora-base:Resource,
[ rdf:type owl:Restriction ;
owl:onProperty :hasName ;
owl:minCardinality "0"^^xsd:nonNegativeInteger ;
salsah-gui:guiOrder "1"^^xsd:nonNegativeInteger ] ;
rdfs:label "Buch mit Kommentar"@de,
"Book with comment"@en ;
rdfs:comment """A comment for book"""@en .

:BookWithoutComment
rdf:type owl:Class ;
rdfs:subClassOf knora-base:Resource,
[ rdf:type owl:Restriction ;
owl:onProperty :hasName ;
owl:minCardinality "0"^^xsd:nonNegativeInteger ;
salsah-gui:guiOrder "1"^^xsd:nonNegativeInteger ] ;
rdfs:label "Buch ohne Kommentar"@de,
"Book without comment"@en .

:BookWithComment2
rdf:type owl:Class ;
rdfs:subClassOf knora-base:Resource,
[ rdf:type owl:Restriction ;
owl:onProperty :hasName ;
owl:minCardinality "0"^^xsd:nonNegativeInteger ;
salsah-gui:guiOrder "1"^^xsd:nonNegativeInteger ] ;
rdfs:label "Buch 2 mit Kommentar"@de,
"Book 2 with comment"@en ;
rdfs:comment """A comment for book"""@en .

:hasFoafName
rdf:type owl:ObjectProperty ;
rdfs:subPropertyOf knora-base:hasValue,
Expand Down Expand Up @@ -348,8 +380,8 @@
:hasPropertyWithComment2
rdf:type owl:ObjectProperty ;
rdfs:subPropertyOf knora-base:hasValue ;
rdfs:label "Property mit einem Kommentar"@de,
"Property with a comment"@en ;
rdfs:label "Property mit einem Kommentar 2"@de,
"Property with a comment 2"@en ;
rdfs:comment "Dies ist der Kommentar"@de,
"This is the comment"@en ;
knora-base:objectClassConstraint knora-base:TextValue ;
Expand Down
Expand Up @@ -1309,6 +1309,80 @@ object ChangeClassLabelsOrCommentsRequestV2 extends KnoraJsonLDRequestReaderV2[C
}
}

/**
* Deletes the comment from a class. A successful response will be a [[ReadOntologyV2]].
*
* @param classIri the IRI of the class.
* @param lastModificationDate the ontology's last modification date
* @param apiRequestID the ID of the API request.
* @param requestingUser the user making the request.
*/
case class DeleteClassCommentRequestV2(
classIri: SmartIri,
lastModificationDate: Instant,
apiRequestID: UUID,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM
) extends OntologiesResponderRequestV2

/**
* Constructs instances of [[DeleteClassCommentRequestV2]] based on JSON-LD input.
*/
object DeleteClassCommentRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteClassCommentRequestV2] {

/**
* Converts a JSON-LD request to a [[DeleteClassCommentRequestV2]].
*
* @param jsonLDDocument the JSON-LD input.
* @param apiRequestID the UUID of the API request.
* @param requestingUser the user making the request.
* @param responderManager a reference to the responder manager.
* @param storeManager a reference to the store manager.
* @param featureFactoryConfig the feature factory configuration.
* @param settings the application settings.
* @param log a logging adapter.
* @return a [[DeleteClassCommentRequestV2]] representing the input.
*/
override def fromJsonLD(
jsonLDDocument: JsonLDDocument,
apiRequestID: UUID,
requestingUser: UserADM,
responderManager: ActorRef,
storeManager: ActorRef,
featureFactoryConfig: FeatureFactoryConfig,
settings: KnoraSettingsImpl,
log: LoggingAdapter
)(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DeleteClassCommentRequestV2] =
Future {
fromJsonLDSync(
jsonLDDocument = jsonLDDocument,
apiRequestID = apiRequestID,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)
}

private def fromJsonLDSync(
jsonLDDocument: JsonLDDocument,
apiRequestID: UUID,
featureFactoryConfig: FeatureFactoryConfig,
requestingUser: UserADM
): DeleteClassCommentRequestV2 = {
val inputOntologyV2: InputOntologyV2 = InputOntologyV2.fromJsonLD(jsonLDDocument)
val classUpdateInfo: ClassUpdateInfo = OntologyUpdateHelper.getClassDef(inputOntologyV2)
val classInfoContent: ClassInfoContentV2 = classUpdateInfo.classInfoContent
val lastModificationDate: Instant = classUpdateInfo.lastModificationDate

DeleteClassCommentRequestV2(
classIri = classInfoContent.classIri,
lastModificationDate = lastModificationDate,
apiRequestID = apiRequestID,
featureFactoryConfig = featureFactoryConfig,
requestingUser = requestingUser
)
}
}

case class ChangeGuiOrderRequestV2(
classInfoContent: ClassInfoContentV2,
lastModificationDate: Instant,
Expand Down
Expand Up @@ -114,6 +114,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
changePropertyLabelsOrComments(changePropertyLabelsOrCommentsRequest)
case deletePropertyCommentRequest: DeletePropertyCommentRequestV2 =>
deletePropertyComment(deletePropertyCommentRequest)
case deleteClassCommentRequest: DeleteClassCommentRequestV2 =>
deleteClassComment(deleteClassCommentRequest)
case changePropertyGuiElementRequest: ChangePropertyGuiElementRequest =>
changePropertyGuiElement(changePropertyGuiElementRequest)
case canDeletePropertyRequest: CanDeletePropertyRequestV2 => canDeleteProperty(canDeletePropertyRequest)
Expand Down Expand Up @@ -3497,4 +3499,164 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
} yield taskResult
}

/**
* Delete the `rdfs:comment` in a class definition.
*
* @param deleteClassCommentRequestV2 the request to delete the class' comment
* @return a [[ReadOntologyV2]] containing the modified class definition.
*/
private def deleteClassComment(
deleteClassCommentRequest: DeleteClassCommentRequestV2
): Future[ReadOntologyV2] = {
def makeTaskFuture(
cacheData: Cache.OntologyCacheData,
internalClassIri: SmartIri,
internalOntologyIri: SmartIri,
ontology: ReadOntologyV2,
classToUpdate: ReadClassInfoV2
): Future[ReadOntologyV2] =
for {

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

currentTime: Instant = Instant.now

// Delete the comment
updateSparql: String = org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.deleteClassComment(
ontologyNamedGraphIri = internalOntologyIri,
ontologyIri = internalOntologyIri,
classIri = internalClassIri,
lastModificationDate = deleteClassCommentRequest.lastModificationDate,
currentTime = currentTime
)
.toString()

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

// Check that the ontology's last modification date was updated.
_ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate(
settings = settings,
storeManager = storeManager,
internalOntologyIri = internalOntologyIri,
expectedLastModificationDate = currentTime,
featureFactoryConfig = deleteClassCommentRequest.featureFactoryConfig
)

// Check that the update was successful.
loadedClassDef: ClassInfoContentV2 <- OntologyHelpers.loadClassDefinition(
settings,
storeManager,
classIri = internalClassIri,
featureFactoryConfig = deleteClassCommentRequest.featureFactoryConfig
)

classDefWithoutComment: ClassInfoContentV2 =
classToUpdate.entityInfoContent.copy(
predicates = classToUpdate.entityInfoContent.predicates.-(
OntologyConstants.Rdfs.Comment.toSmartIri
) // the "-" deletes the entry with the comment
)

_ = if (loadedClassDef != classDefWithoutComment) {
throw InconsistentRepositoryDataException(
s"Attempted to save class definition $classDefWithoutComment, but $loadedClassDef was saved"
)
}

// Update the ontology cache using the new class definition.
newReadClassInfo: ReadClassInfoV2 = ReadClassInfoV2(
entityInfoContent = loadedClassDef,
allBaseClasses = classToUpdate.allBaseClasses
)

updatedOntologyMetadata: OntologyMetadataV2 = ontology.ontologyMetadata.copy(
lastModificationDate = Some(currentTime)
)

updatedOntology: ReadOntologyV2 =
ontology.copy(
ontologyMetadata = updatedOntologyMetadata,
classes = ontology.classes + (internalClassIri -> newReadClassInfo)
)

_ = Cache.storeCacheData(
cacheData.copy(
ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology)
)
)

// Read the data back from the cache.

response: ReadOntologyV2 <- getClassDefinitionsFromOntologyV2(
classIris = Set(internalClassIri),
allLanguages = true,
requestingUser = deleteClassCommentRequest.requestingUser
)

} yield response

for {
requestingUser: UserADM <- FastFuture.successful(deleteClassCommentRequest.requestingUser)

externalClassIri: SmartIri = deleteClassCommentRequest.classIri
externalOntologyIri: SmartIri = externalClassIri.getOntologyFromEntity

_ <- OntologyHelpers.checkOntologyAndEntityIrisForUpdate(
externalOntologyIri = externalOntologyIri,
externalEntityIri = externalClassIri,
requestingUser = requestingUser
)

internalClassIri: SmartIri = externalClassIri.toOntologySchema(InternalSchema)
internalOntologyIri: SmartIri = externalOntologyIri.toOntologySchema(InternalSchema)

cacheData: Cache.OntologyCacheData <- Cache.getCacheData

ontology: ReadOntologyV2 = cacheData.ontologies(internalOntologyIri)

classToUpdate: ReadClassInfoV2 =
ontology.classes.getOrElse(
internalClassIri,
throw NotFoundException(s"Class ${deleteClassCommentRequest.classIri} not found")
)

hasComment: Boolean = classToUpdate.entityInfoContent.predicates.contains(
OntologyConstants.Rdfs.Comment.toSmartIri
)

taskResult: ReadOntologyV2 <-
if (hasComment) for {
// Do the remaining pre-update checks and the update while holding a global ontology cache lock.
taskResult: ReadOntologyV2 <- IriLocker.runWithIriLock(
apiRequestID = deleteClassCommentRequest.apiRequestID,
iri = ONTOLOGY_CACHE_LOCK_IRI,
task = () =>
makeTaskFuture(
cacheData = cacheData,
internalClassIri = internalClassIri,
internalOntologyIri = internalOntologyIri,
ontology = ontology,
classToUpdate = classToUpdate
)
)
} yield taskResult
else {
// not change anything if class has no comment
getClassDefinitionsFromOntologyV2(
classIris = Set(internalClassIri),
allLanguages = true,
requestingUser = deleteClassCommentRequest.requestingUser
)
}
} yield taskResult
}

}

0 comments on commit eca9206

Please sign in to comment.