Skip to content

Commit

Permalink
fix(ontology): cardinality of one can be added to classes as long as …
Browse files Browse the repository at this point in the history
…not used in data (#1958)

* allow adding cardinalities of 1 if class not used in data

* Adjust e2e test

* Update ontology-information.md

* fix indentations ins ScalaDoc

Co-authored-by: Ivan Subotic <400790+subotic@users.noreply.github.com>
  • Loading branch information
irinaschubert and subotic committed Dec 14, 2021
1 parent e8b999f commit 2cebac7
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 13 deletions.
3 changes: 1 addition & 2 deletions docs/03-apis/api-v2/ontology-information.md
Expand Up @@ -1452,8 +1452,7 @@ the property definition, submit the request without those predicates.
### Adding Cardinalities to a Class
If the class is used in data or if it
has a subclass, it is not allowed to add cardinalities `owl:minCardinality` greater than 0 or `owl:cardinality 1` to the class.
If the class (or any of its sub-classes) is used in data, it is not allowed to add cardinalities `owl:minCardinality` greater than 0 or `owl:cardinality 1` to the class.
```
HTTP POST to http://host/v2/ontologies/cardinalities
Expand Down
43 changes: 43 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/responders/Responder.scala
Expand Up @@ -131,6 +131,31 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging {

} yield isEntityUsedResponse.results.bindings.nonEmpty

/**
* Checks whether an instance of a class (or any ob its sub-classes) exists
*
* @param classIri the IRI of the class.
*
* @return `true` if the class is used.
*/
protected def isClassUsedInData(
classIri: SmartIri
): Future[Boolean] =
for {
isClassUsedInDataSparql <- Future(
org.knora.webapi.messages.twirl.queries.sparql.v2.txt
.isClassUsedInData(
triplestore = settings.triplestoreType,
classIri = classIri
)
.toString()
)

isClassUsedInDataResponse: SparqlSelectResult <- (storeManager ? SparqlSelectRequest(isClassUsedInDataSparql))
.mapTo[SparqlSelectResult]

} yield isClassUsedInDataResponse.results.bindings.nonEmpty

/**
* Throws an exception if an entity is used in the triplestore.
*
Expand All @@ -153,6 +178,24 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging {
}
} yield ()

/**
* Throws an exception if a class is used in data.
*
* @param classIri the IRI of the class.
* @param errorFun a function that throws an exception. It will be called if the class is used.
*/
protected def throwIfClassIsUsedInData(
classIri: SmartIri,
errorFun: => Nothing
): Future[Unit] =
for {
classIsUsed: Boolean <- isClassUsedInData(classIri)

_ = if (classIsUsed) {
errorFun
}
} yield ()

/**
* Checks whether an entity with the provided custom IRI exists in the triplestore, if yes, throws an exception.
* If no custom IRI was given, creates a random unused IRI.
Expand Down
Expand Up @@ -1348,15 +1348,13 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
}

_ <- hasCardinality match {
// If there is, check that the class isn't used in data, and that it has no subclasses.
// If there is, check that the class isn't used in data.
case Some((propIri: SmartIri, cardinality: KnoraCardinalityInfo)) =>
throwIfEntityIsUsed(
entityIri = internalClassIri,
throwIfClassIsUsedInData(
classIri = internalClassIri,
errorFun = throw BadRequestException(
s"Cardinality ${cardinality.toString} for $propIri cannot be added to class ${addCardinalitiesRequest.classInfoContent.classIri}, because it is used in data or has a subclass"
),
ignoreKnoraConstraints =
true // It's OK if a property refers to the class via knora-base:subjectClassConstraint or knora-base:objectClassConstraint.
s"Cardinality ${cardinality.toString} for $propIri cannot be added to class ${addCardinalitiesRequest.classInfoContent.classIri}, because it is used in data"
)
)
case None => Future.successful(())
}
Expand Down
@@ -0,0 +1,33 @@
@*
* Copyright © 2021 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*@

@import org.knora.webapi.messages.SmartIri

@*
* Checks if an instance of a class exists (if yes, that means the class is used in data)
*
* @param triplestore the name of the triplestore being used.
* @param classIri the IRI of the class.
*@
@(triplestore: String,
classIri: SmartIri)

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT DISTINCT ?isUsed

WHERE {
BIND(IRI("@classIri") AS ?class)
BIND(true AS ?isUsed)

{
?resourceInstance rdf:type ?class
}
UNION{
?subClass rdfs:subClassOf* ?class .
?resourceInstance rdf:type ?subClass
}
}
Expand Up @@ -3480,7 +3480,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
}
}

"not add a cardinality=1 to the class anything:Nothing, because it has a subclass" in {
"add a cardinality=1 to the class anything:Nothing which has a subclass" in {
val classIri = AnythingOntologyIri.makeEntityIri("Nothing")

val classInfoContent = ClassInfoContentV2(
Expand All @@ -3505,9 +3505,34 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
requestingUser = anythingAdminUser
)

expectMsgPF(timeout) { case msg: akka.actor.Status.Failure =>
if (printErrorMessages) println(msg.cause.getMessage)
msg.cause.isInstanceOf[BadRequestException] should ===(true)
expectMsgPF(timeout) { case msg: ReadOntologyV2 =>
val externalOntology = msg.toOntologySchema(ApiV2Complex)
assert(externalOntology.classes.size == 1)

val metadata = externalOntology.ontologyMetadata
val newAnythingLastModDate = metadata.lastModificationDate.getOrElse(
throw AssertionException(s"${metadata.ontologyIri} has no last modification date")
)
assert(newAnythingLastModDate.isAfter(anythingLastModDate))
anythingLastModDate = newAnythingLastModDate
}

responderManager ! DeleteCardinalitiesFromClassRequestV2(
classInfoContent = classInfoContent,
lastModificationDate = anythingLastModDate,
apiRequestID = UUID.randomUUID,
featureFactoryConfig = defaultFeatureFactoryConfig,
requestingUser = anythingAdminUser
)

expectMsgPF(timeout) { case msg: ReadOntologyV2 =>
val externalOntology = msg.toOntologySchema(ApiV2Complex)
val metadata = externalOntology.ontologyMetadata
val newAnythingLastModDate = metadata.lastModificationDate.getOrElse(
throw AssertionException(s"${metadata.ontologyIri} has no last modification date")
)
assert(newAnythingLastModDate.isAfter(anythingLastModDate))
anythingLastModDate = newAnythingLastModDate
}
}

Expand Down

0 comments on commit 2cebac7

Please sign in to comment.