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

feat(ontology): allow deleting comments of classes (DEV-804) #2048

Merged
merged 18 commits into from Apr 26, 2022
Merged
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
}

}