From beb951db46e9d73631667c40168a67dc07a9605c Mon Sep 17 00:00:00 2001 From: Benjamin Geer Date: Wed, 19 May 2021 13:00:45 +0200 Subject: [PATCH] fix(api-v2): Update subclasses in ontology cache when base class changes (DSP-1643) (#1860) --- .../ontologymessages/OntologyMessagesV2.scala | 9 +- .../responders/v2/OntologyResponderV2.scala | 237 +++++++++++------- .../v2/OntologyResponderV2Spec.scala | 62 ++++- 3 files changed, 215 insertions(+), 93 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala index 635706ee87..11046e6720 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala @@ -1943,7 +1943,14 @@ object Cardinality extends Enumeration { * @param guiOrder the SALSAH GUI order. */ case class KnoraCardinalityInfo(cardinality: Value, guiOrder: Option[Int] = None) { - override def toString: String = cardinality.toString + override def toString: String = guiOrder match { + case Some(definedGuiOrder) => s"$cardinality (guiOrder $definedGuiOrder)" + case None => cardinality.toString + } + + def equalsWithoutGuiOrder(that: KnoraCardinalityInfo): Boolean = { + that.cardinality == cardinality + } } type Cardinality = Value diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala index d5ec50b725..b9f155b267 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala @@ -36,9 +36,9 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.{ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages._ import org.knora.webapi.messages.util.rdf.{SparqlSelectResult, VariableResultsRow} -import org.knora.webapi.messages.util.{ErrorHandlingMap, KnoraSystemInstances, OntologyUtil, ResponderData} +import org.knora.webapi.messages.util.{ErrorHandlingMap, KnoraSystemInstances, MessageUtil, OntologyUtil, ResponderData} import org.knora.webapi.messages.v2.responder.SuccessResponseV2 -import org.knora.webapi.messages.v2.responder.ontologymessages.Cardinality.{KnoraCardinalityInfo, OwlCardinalityInfo} +import org.knora.webapi.messages.v2.responder.ontologymessages.Cardinality.KnoraCardinalityInfo import org.knora.webapi.messages.v2.responder.ontologymessages._ import org.knora.webapi.messages.v2.responder.standoffmessages.StandoffDataTypeClasses import org.knora.webapi.messages.{OntologyConstants, SmartIri} @@ -354,19 +354,16 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon allSubPropertyOfRelations(prop).contains(OntologyConstants.KnoraBase.HasFileValue.toSmartIri)) // A map of the cardinalities defined directly on each resource class. Each class IRI points to a map of - // property IRIs to OwlCardinalityInfo objects. - val directClassCardinalities: Map[SmartIri, Map[SmartIri, OwlCardinalityInfo]] = allClassDefs.map { + // property IRIs to KnoraCardinalityInfo objects. + val directClassCardinalities: Map[SmartIri, Map[SmartIri, KnoraCardinalityInfo]] = allClassDefs.map { case (classIri, classDef) => - classIri -> classDef.directCardinalities.map { - case (propertyIri, knoraCardinalityInfo) => - propertyIri -> Cardinality.knoraCardinality2OwlCardinality(knoraCardinalityInfo) - } + classIri -> classDef.directCardinalities } // Allow each class to inherit cardinalities from its base classes. - val classCardinalitiesWithInheritance: Map[SmartIri, Map[SmartIri, OwlCardinalityInfo]] = allClassIris.map { + val classCardinalitiesWithInheritance: Map[SmartIri, Map[SmartIri, KnoraCardinalityInfo]] = allClassIris.map { resourceClassIri => - val resourceClassCardinalities: Map[SmartIri, OwlCardinalityInfo] = inheritCardinalitiesInLoadedClass( + val resourceClassCardinalities: Map[SmartIri, KnoraCardinalityInfo] = inheritCardinalitiesInLoadedClass( classIri = resourceClassIri, directSubClassOfRelations = directSubClassOfRelations, allSubPropertyOfRelations = allSubPropertyOfRelations, @@ -780,10 +777,10 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon * * @param classDefs a map of class IRIs to class definitions. * @param directClassCardinalities a map of the cardinalities defined directly on each class. Each resource class - * IRI points to a map of property IRIs to [[OwlCardinalityInfo]] objects. + * IRI points to a map of property IRIs to [[KnoraCardinalityInfo]] objects. * @param classCardinalitiesWithInheritance a map of the cardinalities defined directly on each class or inherited from * base classes. Each class IRI points to a map of property IRIs to - * [[OwlCardinalityInfo]] objects. + * [[KnoraCardinalityInfo]] objects. * @param directSubClassOfRelations a map of class IRIs to their immediate base classes. * @param allSubClassOfRelations a map of class IRIs to all their base classes. * @param allSubPropertyOfRelations a map of property IRIs to all their base properties. @@ -795,8 +792,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon * @return a map of resource class IRIs to their definitions. */ private def makeReadClassInfos(classDefs: Map[SmartIri, ClassInfoContentV2], - directClassCardinalities: Map[SmartIri, Map[SmartIri, OwlCardinalityInfo]], - classCardinalitiesWithInheritance: Map[SmartIri, Map[SmartIri, OwlCardinalityInfo]], + directClassCardinalities: Map[SmartIri, Map[SmartIri, KnoraCardinalityInfo]], + classCardinalitiesWithInheritance: Map[SmartIri, Map[SmartIri, KnoraCardinalityInfo]], directSubClassOfRelations: Map[SmartIri, Set[SmartIri]], allSubClassOfRelations: Map[SmartIri, Seq[SmartIri]], allSubPropertyOfRelations: Map[SmartIri, Set[SmartIri]], @@ -810,7 +807,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon val ontologyIri = classIri.getOntologyFromEntity // Get the OWL cardinalities for the class. - val allOwlCardinalitiesForClass: Map[SmartIri, OwlCardinalityInfo] = classCardinalitiesWithInheritance(classIri) + val allOwlCardinalitiesForClass: Map[SmartIri, KnoraCardinalityInfo] = + classCardinalitiesWithInheritance(classIri) val allPropertyIrisForCardinalitiesInClass: Set[SmartIri] = allOwlCardinalitiesForClass.keys.toSet // Identify the Knora resource properties, link properties, link value properties, and file value properties in the cardinalities. @@ -842,8 +840,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Make sure that the cardinality for each link property is the same as the cardinality for the corresponding link value property. for (linkProp <- linkPropsInClass) { val linkValueProp: SmartIri = linkProp.fromLinkPropToLinkValueProp - val linkPropCardinality: OwlCardinalityInfo = allOwlCardinalitiesForClass(linkProp) - val linkValuePropCardinality: OwlCardinalityInfo = allOwlCardinalitiesForClass(linkValueProp) + val linkPropCardinality: KnoraCardinalityInfo = allOwlCardinalitiesForClass(linkProp) + val linkValuePropCardinality: KnoraCardinalityInfo = allOwlCardinalitiesForClass(linkValueProp) if (!linkPropCardinality.equalsWithoutGuiOrder(linkValuePropCardinality)) { throw InconsistentRepositoryDataException( @@ -852,12 +850,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon } // The class's direct cardinalities. - - val directCardinalities: Map[SmartIri, KnoraCardinalityInfo] = directClassCardinalities(classIri).map { - case (propertyIri, owlCardinalityInfo) => - propertyIri -> Cardinality.owlCardinality2KnoraCardinality(propertyIri = propertyIri.toString, - owlCardinality = owlCardinalityInfo) - } + val directCardinalities: Map[SmartIri, KnoraCardinalityInfo] = directClassCardinalities(classIri) val directCardinalityPropertyIris = directCardinalities.keySet val allBaseClasses: Seq[SmartIri] = allSubClassOfRelations(classIri) @@ -981,11 +974,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon .filterNot { case (propertyIri, _) => directCardinalityPropertyIris.contains(propertyIri) } - .map { - case (propertyIri, owlCardinalityInfo) => - propertyIri -> Cardinality.owlCardinality2KnoraCardinality(propertyIri = propertyIri.toString, - owlCardinality = owlCardinalityInfo) - } // Get the class's standoff data type, if any. A standoff class that has a datatype is a subclass of one of the classes // in StandoffDataTypeClasses. @@ -2584,7 +2572,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon linkValuePropIri -> internalClassDef.directCardinalities(linkPropIri) }.toMap - val classDefWithAddedLinkValueProps = internalClassDef.copy( + val classDefWithAddedLinkValueProps: ClassInfoContentV2 = internalClassDef.copy( directCardinalities = internalClassDef.directCardinalities ++ linkValuePropCardinalitiesToAdd ) @@ -2598,29 +2586,16 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the cardinalities directly defined on the class are compatible with any inheritable // cardinalities, and let directly-defined cardinalities override cardinalities in base classes. - val thisClassKnoraCardinalities = classDefWithAddedLinkValueProps.directCardinalities.map { - case (propertyIri, knoraCardinality) => - propertyIri -> Cardinality.knoraCardinality2OwlCardinality(knoraCardinality) - } - - val inheritableKnoraCardinalities = cardinalitiesAvailableToInherit.map { - case (propertyIri, knoraCardinality) => - propertyIri -> Cardinality.knoraCardinality2OwlCardinality(knoraCardinality) - } - val cardinalitiesForClassWithInheritance: Map[SmartIri, KnoraCardinalityInfo] = overrideCardinalities( classIri = internalClassDef.classIri, - thisClassCardinalities = thisClassKnoraCardinalities, - inheritableCardinalities = inheritableKnoraCardinalities, + thisClassCardinalities = classDefWithAddedLinkValueProps.directCardinalities, + inheritableCardinalities = cardinalitiesAvailableToInherit, allSubPropertyOfRelations = cacheData.subPropertyOfRelations, - errorSchema = ApiV2Complex, { msg: String => + errorSchema = ApiV2Complex, + errorFun = { msg: String => throw BadRequestException(msg) } - ).map { - case (propertyIri, owlCardinalityInfo) => - propertyIri -> Cardinality.owlCardinality2KnoraCardinality(propertyIri = propertyIri.toString, - owlCardinality = owlCardinalityInfo) - } + ) // Check that the class is a subclass of all the classes that are subject class constraints of the Knora resource properties in its cardinalities. @@ -2676,6 +2651,78 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon (classDefWithAddedLinkValueProps, cardinalitiesForClassWithInheritance) } + /** + * Given the IRI of a base class, updates inherited cardinalities in subclasses. + * + * @param baseClassIri the internal IRI of the base class. + * @param cacheData the ontology cache. + * + * @return the updated ontology cache. + */ + private def updateSubClasses(baseClassIri: SmartIri, cacheData: OntologyCacheData): OntologyCacheData = { + // Get the class definitions of all the subclasses of the base class. + + val allSubClassIris: Set[SmartIri] = cacheData.superClassOfRelations(baseClassIri) + + val allSubClasses: Set[ReadClassInfoV2] = allSubClassIris.map { subClassIri => + cacheData.ontologies(subClassIri.getOntologyFromEntity).classes(subClassIri) + } + + // Filter them to get only the direct subclasses. + + val directSubClasses: Set[ReadClassInfoV2] = allSubClasses.filter { subClass => + subClass.entityInfoContent.subClassOf + .contains(baseClassIri) && subClass.entityInfoContent.classIri != baseClassIri + } + + // Iterate over the subclasses, updating cardinalities. + val cacheDataWithUpdatedSubClasses = directSubClasses.foldLeft(cacheData) { + case (cacheDataAcc: OntologyCacheData, directSubClass: ReadClassInfoV2) => + val directSubClassIri = directSubClass.entityInfoContent.classIri + + // Get the cardinalities that this subclass can inherit from its direct base classes. + + val inheritableCardinalities: Map[SmartIri, KnoraCardinalityInfo] = + directSubClass.entityInfoContent.subClassOf.flatMap { baseClassIri => + cacheData.ontologies(baseClassIri.getOntologyFromEntity).classes(baseClassIri).allCardinalities + }.toMap + + // Override inherited cardinalities with directly defined cardinalities. + val newInheritedCardinalities: Map[SmartIri, KnoraCardinalityInfo] = overrideCardinalities( + classIri = directSubClassIri, + thisClassCardinalities = directSubClass.entityInfoContent.directCardinalities, + inheritableCardinalities = inheritableCardinalities, + allSubPropertyOfRelations = cacheData.subPropertyOfRelations, + errorSchema = ApiV2Complex, + errorFun = { msg: String => + throw BadRequestException(msg) + } + ) + + // Update the cache. + + val ontologyIri = directSubClass.entityInfoContent.classIri.getOntologyFromEntity + val ontology: ReadOntologyV2 = cacheDataAcc.ontologies(ontologyIri) + + val updatedOntology = ontology.copy( + classes = ontology.classes + (directSubClassIri -> directSubClass.copy( + inheritedCardinalities = newInheritedCardinalities + )) + ) + + cacheDataAcc.copy( + ontologies = cacheDataAcc.ontologies + (ontologyIri -> updatedOntology) + ) + } + + // Recurse to subclasses of subclasses. + + directSubClasses.map(_.entityInfoContent.classIri).foldLeft(cacheDataWithUpdatedSubClasses) { + case (cacheDataAcc: OntologyCacheData, directSubClassIri: SmartIri) => + updateSubClasses(baseClassIri = directSubClassIri, cacheDataAcc) + } + } + /** * Given a set of property IRIs, determines whether the set contains a property P and a subproperty of P. * @@ -2907,7 +2954,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon s"Attempted to save class definition $newInternalClassDefWithLinkValueProps, but $loadedClassDef was saved") } - // Update the cache. + // Update subclasses and write the cache. updatedOntology = ontology.copy( ontologyMetadata = ontology.ontologyMetadata.copy( @@ -2917,9 +2964,10 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon ) _ = storeCacheData( - cacheData.copy( - ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology) - )) + updateSubClasses(baseClassIri = internalClassIri, + cacheData = cacheData.copy( + ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology) + ))) // Read the data back from the cache. @@ -2959,6 +3007,12 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon } yield taskResult } + /** + * Changes GUI orders in cardinalities in a class definition. + * + * @param changeGuiOrderRequest the request message. + * @return the updated class definition. + */ private def changeGuiOrder(changeGuiOrderRequest: ChangeGuiOrderRequestV2): Future[ReadOntologyV2] = { def makeTaskFuture(internalClassIri: SmartIri, internalOntologyIri: SmartIri): Future[ReadOntologyV2] = { for { @@ -3001,13 +3055,26 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon .mkString(", ")}") } + linkValuePropCardinalities = internalClassDef.directCardinalities + .filter { + case (propertyIri: SmartIri, _: KnoraCardinalityInfo) => + val propertyDef = cacheData.ontologies(propertyIri.getOntologyFromEntity).properties(propertyIri) + propertyDef.isLinkProp + } + .map { + case (propertyIri: SmartIri, cardinalityWithCurrentGuiOrder: KnoraCardinalityInfo) => + propertyIri.fromLinkPropToLinkValueProp -> cardinalityWithCurrentGuiOrder + } + + internalClassDefWithLinkValueProps = internalClassDef.directCardinalities ++ linkValuePropCardinalities + // Make an updated class definition. newReadClassInfo = currentReadClassInfo.copy( entityInfoContent = currentReadClassInfo.entityInfoContent.copy( directCardinalities = currentReadClassInfo.entityInfoContent.directCardinalities.map { case (propertyIri: SmartIri, cardinalityWithCurrentGuiOrder: KnoraCardinalityInfo) => - internalClassDef.directCardinalities.get(propertyIri) match { + internalClassDefWithLinkValueProps.get(propertyIri) match { case Some(cardinalityWithNewGuiOrder) => propertyIri -> cardinalityWithCurrentGuiOrder.copy(guiOrder = cardinalityWithNewGuiOrder.guiOrder) @@ -3064,10 +3131,13 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon classes = ontology.classes + (internalClassIri -> newReadClassInfo) ) + // Update subclasses and write the cache. + _ = storeCacheData( - cacheData.copy( - ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology) - )) + updateSubClasses(baseClassIri = internalClassIri, + cacheData = cacheData.copy( + ontologies = cacheData.ontologies + (internalOntologyIri -> updatedOntology) + ))) // Read the data back from the cache. @@ -3076,6 +3146,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon allLanguages = true, requestingUser = changeGuiOrderRequest.requestingUser ) + } yield response } @@ -5135,11 +5206,11 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon * from its base classes), and each value is the cardinality on the property. */ private def overrideCardinalities(classIri: SmartIri, - thisClassCardinalities: Map[SmartIri, OwlCardinalityInfo], - inheritableCardinalities: Map[SmartIri, OwlCardinalityInfo], + thisClassCardinalities: Map[SmartIri, KnoraCardinalityInfo], + inheritableCardinalities: Map[SmartIri, KnoraCardinalityInfo], allSubPropertyOfRelations: Map[SmartIri, Set[SmartIri]], errorSchema: OntologySchema, - errorFun: String => Nothing): Map[SmartIri, OwlCardinalityInfo] = { + errorFun: String => Nothing): Map[SmartIri, KnoraCardinalityInfo] = { // A map of directly defined properties to the base class properties they can override. val overrides: Map[SmartIri, Set[SmartIri]] = thisClassCardinalities.map { case (thisClassProp, thisClassCardinality) => @@ -5149,29 +5220,18 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon val basePropsOfThisClassProp: Set[SmartIri] = allSubPropertyOfRelations.getOrElse(thisClassProp, Set.empty[SmartIri]) - val overridedBaseProps: Set[SmartIri] = inheritableCardinalities.foldLeft(Set.empty[SmartIri]) { + val overriddenBaseProps: Set[SmartIri] = inheritableCardinalities.foldLeft(Set.empty[SmartIri]) { case (acc, (baseClassProp, baseClassCardinality)) => // Can the directly defined cardinality override the inheritable one? if (thisClassProp == baseClassProp || basePropsOfThisClassProp.contains(baseClassProp)) { // Yes. Is the directly defined one at least as restrictive as the inheritable one? - val thisClassKnoraCardinality: KnoraCardinalityInfo = Cardinality.owlCardinality2KnoraCardinality( - propertyIri = thisClassProp.toString, - owlCardinality = thisClassCardinality - ) - - val inheritableKnoraCardinality: KnoraCardinalityInfo = Cardinality.owlCardinality2KnoraCardinality( - propertyIri = baseClassProp.toString, - owlCardinality = baseClassCardinality - ) - - if (!Cardinality.isCompatible(directCardinality = thisClassKnoraCardinality.cardinality, - inheritableCardinality = inheritableKnoraCardinality.cardinality)) { + if (!Cardinality.isCompatible(directCardinality = thisClassCardinality.cardinality, + inheritableCardinality = baseClassCardinality.cardinality)) { // No. Throw an exception. - errorFun( - s"In class <${classIri.toOntologySchema(errorSchema)}>, the directly defined cardinality $thisClassKnoraCardinality on ${thisClassProp.toOntologySchema( - errorSchema)} is not compatible with the inherited cardinality $inheritableKnoraCardinality on ${baseClassProp - .toOntologySchema(errorSchema)}, because it is less restrictive") + errorFun(s"In class <${classIri.toOntologySchema(errorSchema)}>, the directly defined cardinality ${thisClassCardinality.cardinality} on ${thisClassProp.toOntologySchema( + errorSchema)} is not compatible with the inherited cardinality ${baseClassCardinality.cardinality} on ${baseClassProp + .toOntologySchema(errorSchema)}, because it is less restrictive") } else { // Yes. Filter out the inheritable one, because the directly defined one overrides it. acc + baseClassProp @@ -5182,19 +5242,18 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon } } - thisClassProp -> overridedBaseProps + thisClassProp -> overriddenBaseProps } // A map of base class properties to the directly defined properties that can override them. - val reverseOverrides: Map[SmartIri, Set[SmartIri]] = overrides - .map { + val reverseOverrides: Map[SmartIri, Set[SmartIri]] = overrides.toVector + .flatMap { // Unpack the sets to make an association list. case (thisClassProp: SmartIri, baseClassProps: Set[SmartIri]) => - baseClassProps.map { baseClassProp => + baseClassProps.map { baseClassProp: SmartIri => thisClassProp -> baseClassProp } } - .flatten .map { // Reverse the direction of the association list. case (thisClassProp: SmartIri, baseClassProp: SmartIri) => @@ -5218,10 +5277,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon reverseOverrides.foreach { case (baseClassProp, thisClassProps) => if (thisClassProps.size > 1) { - val overriddenCardinality: KnoraCardinalityInfo = Cardinality.owlCardinality2KnoraCardinality( - propertyIri = baseClassProp.toString, - owlCardinality = inheritableCardinalities(baseClassProp) - ) + val overriddenCardinality: KnoraCardinalityInfo = inheritableCardinalities(baseClassProp) if (overriddenCardinality.cardinality == Cardinality.MustHaveOne || overriddenCardinality.cardinality == Cardinality.MayHaveOne) { errorFun( @@ -5274,14 +5330,15 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon classIri: SmartIri, directSubClassOfRelations: Map[SmartIri, Set[SmartIri]], allSubPropertyOfRelations: Map[SmartIri, Set[SmartIri]], - directClassCardinalities: Map[SmartIri, Map[SmartIri, OwlCardinalityInfo]]): Map[SmartIri, OwlCardinalityInfo] = { + directClassCardinalities: Map[SmartIri, Map[SmartIri, KnoraCardinalityInfo]]) + : Map[SmartIri, KnoraCardinalityInfo] = { // Recursively get properties that are available to inherit from base classes. If we have no information about // a class, that could mean that it isn't a subclass of knora-base:Resource (e.g. it's something like // foaf:Person), in which case we assume that it has no base classes. - val cardinalitiesAvailableToInherit: Map[SmartIri, OwlCardinalityInfo] = directSubClassOfRelations + val cardinalitiesAvailableToInherit: Map[SmartIri, KnoraCardinalityInfo] = directSubClassOfRelations .getOrElse(classIri, Set.empty[SmartIri]) - .foldLeft(Map.empty[SmartIri, OwlCardinalityInfo]) { - case (acc, baseClass) => + .foldLeft(Map.empty[SmartIri, KnoraCardinalityInfo]) { + case (acc: Map[SmartIri, KnoraCardinalityInfo], baseClass: SmartIri) => acc ++ inheritCardinalitiesInLoadedClass( classIri = baseClass, directSubClassOfRelations = directSubClassOfRelations, @@ -5292,8 +5349,8 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Get the properties that have cardinalities defined directly on this class. Again, if we have no information // about a class, we assume that it has no cardinalities. - val thisClassCardinalities: Map[SmartIri, OwlCardinalityInfo] = - directClassCardinalities.getOrElse(classIri, Map.empty[SmartIri, OwlCardinalityInfo]) + val thisClassCardinalities: Map[SmartIri, KnoraCardinalityInfo] = + directClassCardinalities.getOrElse(classIri, Map.empty[SmartIri, KnoraCardinalityInfo]) // Combine the cardinalities defined directly on this class with the ones that are available to inherit. overrideCardinalities( diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala index 77c691f164..dc3371fb96 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala @@ -72,7 +72,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { private val ExampleSharedOntologyIri = "http://api.knora.org/ontology/shared/example-box/v2".toSmartIri private val IncunabulaOntologyIri = "http://0.0.0.0:3333/ontology/0803/incunabula/v2".toSmartIri private val AnythingOntologyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2".toSmartIri - private var anythingLastModDate: Instant = Instant.now + private var anythingLastModDate: Instant = Instant.parse("2017-12-19T15:23:42.166Z") private val printErrorMessages = false @@ -4032,7 +4032,7 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { ontologySchema = ApiV2Complex ) - responderManager ! ChangeCardinalitiesRequestV2( + responderManager ! ChangeGuiOrderRequestV2( classInfoContent = classInfoContent, lastModificationDate = anythingLastModDate, apiRequestID = UUID.randomUUID, @@ -5237,6 +5237,64 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { } } + "change the GUI order of a cardinality in a base class, and update its subclass in the ontology cache" in { + val classIri = AnythingOntologyIri.makeEntityIri("Thing") + + val newCardinality = KnoraCardinalityInfo(cardinality = Cardinality.MayHaveMany, guiOrder = Some(100)) + + 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("hasText") -> newCardinality + ), + ontologySchema = ApiV2Complex + ) + + responderManager ! ChangeGuiOrderRequestV2( + 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 readClassInfo = externalOntology.classes(classIri) + assert( + readClassInfo.entityInfoContent + .directCardinalities(AnythingOntologyIri.makeEntityIri("hasText")) == newCardinality) + + 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 ! ClassesGetRequestV2( + classIris = Set(AnythingOntologyIri.makeEntityIri("ThingWithSeqnum")), + allLanguages = false, + requestingUser = anythingAdminUser + ) + + expectMsgPF(timeout) { + case msg: ReadOntologyV2 => + val externalOntology = msg.toOntologySchema(ApiV2Complex) + assert(externalOntology.classes.size == 1) + val readClassInfo = externalOntology.classes(AnythingOntologyIri.makeEntityIri("ThingWithSeqnum")) + assert(readClassInfo.inheritedCardinalities(AnythingOntologyIri.makeEntityIri("hasText")) == newCardinality) + } + } + "not load an ontology that has no knora-base:attachedToProject" in { val invalidOnto = List( RdfDataObject(