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 adding new property to a resource class in use (DSP-1629) #1859

Merged
merged 9 commits into from May 18, 2021
8 changes: 4 additions & 4 deletions docs/03-apis/api-v2/ontology-information.md
Expand Up @@ -1449,8 +1449,8 @@ the property definition, submit the request without those predicates.

### Adding Cardinalities to a Class

This operation is not permitted if the class is used in data, or if it
has a subclass.
If the class is used in data or if it
has a subclass, it is not allowed to add cardinalities `owl:minCardinality` greater than 1 or `owl:cardinality 1` to the class.

```
HTTP POST to http://host/v2/ontologies/cardinalities
Expand All @@ -1464,7 +1464,7 @@ HTTP POST to http://host/v2/ontologies/cardinalities
"@type" : "xsd:dateTimeStamp",
"@value" : "ONTOLOGY_LAST_MODIFICATION_DATE"
},
"@graph" : [ {
"@graph" : [
{
"@id" : "CLASS_IRI",
"@type" : "owl:Class",
Expand All @@ -1476,7 +1476,7 @@ HTTP POST to http://host/v2/ontologies/cardinalities
}
}
}
} ],
],
"@context" : {
"knora-api" : "http://api.knora.org/ontology/knora-api/v2#",
"owl" : "http://www.w3.org/2002/07/owl#",
Expand Down
2 changes: 1 addition & 1 deletion test_data/ontologies/anything-onto.ttl
Expand Up @@ -634,7 +634,7 @@
rdf:type owl:Restriction ;
owl:onProperty :hasBlueThingValue ;
owl:minCardinality "0"^^xsd:nonNegativeInteger ;
salsah-gui:guiOrder "16"^^xsd:nonNegativeInteger
salsah-gui:guiOrder "63"^^xsd:nonNegativeInteger
] ;

rdfs:comment """Diese Resource-Klasse beschreibt ein blaues Ding"""@de ;
Expand Down
Expand Up @@ -2795,14 +2795,24 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon
.mkString(", ")}")
}

// Check that the class isn't used in data, and that it has no subclasses.
// Is there any property with minCardinality>0 or Cardinality=1?
hasCardinality: Option[(SmartIri, KnoraCardinalityInfo)] = addCardinalitiesRequest.classInfoContent.directCardinalities
.find {
case (_, constraint: KnoraCardinalityInfo) =>
constraint.cardinality == Cardinality.MustHaveSome || constraint.cardinality == Cardinality.MustHaveOne
}

_ <- isEntityUsed(
entityIri = internalClassIri,
errorFun = throw BadRequestException(
s"Cardinalities 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.
)
_ <- hasCardinality match {
// If there is, check that the class isn't used in data, and that it has no subclasses.
case Some((propIri: SmartIri, cardinality: KnoraCardinalityInfo)) =>
isEntityUsed(
entityIri = 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.
)
case None => Future.successful(())
}

// Make an updated class definition.

Expand Down
Expand Up @@ -3464,7 +3464,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
}
}

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

val classInfoContent = ClassInfoContentV2(
Expand All @@ -3476,7 +3476,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
)
),
directCardinalities = Map(
AnythingOntologyIri.makeEntityIri("hasNothingness") -> KnoraCardinalityInfo(Cardinality.MayHaveOne)
AnythingOntologyIri.makeEntityIri("hasNothingness") -> KnoraCardinalityInfo(Cardinality.MustHaveOne)
),
ontologySchema = ApiV2Complex
)
Expand Down Expand Up @@ -3775,7 +3775,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
}
}

"not add a cardinality for property anything:hasName to class anything:BlueThing, because the class is used in data" in {
"not add a minCardinality=1 for property anything:hasName to class anything:BlueThing, because the class is used in data" in {
val classIri = AnythingOntologyIri.makeEntityIri("BlueThing")

val classInfoContent = ClassInfoContentV2(
Expand All @@ -3787,7 +3787,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
)
),
directCardinalities = Map(
AnythingOntologyIri.makeEntityIri("hasName") -> KnoraCardinalityInfo(Cardinality.MayHaveOne)
AnythingOntologyIri.makeEntityIri("hasName") -> KnoraCardinalityInfo(Cardinality.MustHaveSome)
),
ontologySchema = ApiV2Complex
)
Expand All @@ -3807,6 +3807,44 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender {
}
}

"add a maxCardinality=1 for property anything:hasName to class anything:BlueThing even though the class is used in data" in {
val classIri = AnythingOntologyIri.makeEntityIri("BlueThing")

val classInfoContent = ClassInfoContentV2(
classIri = classIri,
predicates = Map(
OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2(
predicateIri = OntologyConstants.Rdf.Type.toSmartIri,
objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri))
)
),
directCardinalities = Map(
AnythingOntologyIri.makeEntityIri("hasName") -> KnoraCardinalityInfo(Cardinality.MayHaveOne)
),
ontologySchema = ApiV2Complex
)

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

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
}
}

"create a property anything:hasEmptiness with knora-api:subjectType anything:Nothing" in {
val propertyIri = AnythingOntologyIri.makeEntityIri("hasEmptiness")

Expand Down