diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/CardinalityService.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/CardinalityService.scala index aaebb961d3..39fab5767b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/CardinalityService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/CardinalityService.scala @@ -10,8 +10,6 @@ import zio.ZIO import zio.ZLayer import zio.macros.accessible -import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages._ import org.knora.webapi.messages.v2.responder.ontologymessages.ClassInfoContentV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 @@ -194,7 +192,7 @@ final case class CardinalityServiceLive( val subclasses = for { subclasses <- ontologyRepo.findAllSubclassesBy(check.classIri) - superClasses <- ontologyRepo.findAllSuperClassesBy(toClassIris(subclasses)) + superClasses <- ontologyRepo.findAllSuperClassesBy(toClassIris(subclasses), upToClass = check.classIri) } yield subclasses ::: superClasses val subclassCardinalityIsNotIncluded = (other: Cardinality) => other.isNotIncludedIn(check.newCardinality) canSetCheckFor(subclasses, check.propertyIri, subclassCardinalityIsNotIncluded, SubclassCheckFailure) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala index a0ddc3f0ea..02e06df6a7 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala @@ -28,6 +28,15 @@ trait OntologyRepo extends Repository[ReadOntologyV2, InternalIri] { def findAllSuperClassesBy(classIris: List[InternalIri]): Task[List[ReadClassInfoV2]] + /** + * Finds all super-classes of a particular class up to the given class in a hierarchy. + * + * @param classIris the classes to find the super-classes for + * @param upToClass the class to stop the search at for the particular branch in the class hierarchy + * @return all the super-classes of all other branches in the class hierarchy + */ + def findAllSuperClassesBy(classIris: List[InternalIri], upToClass: InternalIri): Task[List[ReadClassInfoV2]] + def findDirectSubclassesBy(classIri: InternalIri): Task[List[ReadClassInfoV2]] def findAllSubclassesBy(classIri: InternalIri): Task[List[ReadClassInfoV2]] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala index 659432d7b0..167c1b95fd 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala @@ -102,17 +102,33 @@ final case class OntologyRepoLive(private val converter: IriConverter, private v override def findAllSuperClassesBy(classIris: List[InternalIri]): Task[List[ReadClassInfoV2]] = smartIrisMapCache(classIris)((iris, cache) => findAllSuperClassesBy(iris, List.empty, cache)) + override def findAllSuperClassesBy( + classIris: List[InternalIri], + upToClass: InternalIri + ): Task[List[ReadClassInfoV2]] = + for { + upToClassIri <- toSmartIri(upToClass) + result <- smartIrisMapCache(classIris)((iris, cache) => + findAllSuperClassesBy(iris, List.empty, cache, Some(upToClassIri)) + ) + } yield result.distinct + @tailrec private def findAllSuperClassesBy( classIris: List[SmartIri], acc: List[ReadClassInfoV2], - cache: OntologyCacheData + cache: OntologyCacheData, + upToClassIri: Option[SmartIri] = None ): List[ReadClassInfoV2] = { val superClassesWithSelf = findDirectSuperClassesBy(classIris, cache) val superClasses = superClassesWithSelf.filter(it => !classIris.contains(it.entityInfoContent.classIri)) - superClasses match { + val filteredSuperClasses = upToClassIri match { + case Some(iri) => superClasses.filter(_.entityInfoContent.classIri != iri) + case None => superClasses + } + filteredSuperClasses match { case Nil => acc - case classes => findAllSuperClassesBy(toClassIris(classes), acc ::: classes, cache) + case classes => findAllSuperClassesBy(toClassIris(classes), acc ::: classes, cache, upToClassIri) } } } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/CardinalityServiceLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/CardinalityServiceLiveSpec.scala index 02a6dc35cd..02b459e4bd 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/CardinalityServiceLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/CardinalityServiceLiveSpec.scala @@ -110,7 +110,7 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { .addClassInfo( ReadClassInfoV2Builder .builder(classIri) - .setDirectCardinalities(cardinalities) + .addProperty(propertyIri.toInternalIri, cardinality) ) .addClassInfo( ReadClassInfoV2Builder @@ -139,7 +139,6 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { subClassIri: SmartIri, propertyIri: SmartIri ): DataCreated = { - val cardinalities = OntologyCacheDataBuilder.cardinalitiesMap(propertyIri, cardinality) val data = OntologyCacheDataBuilder.builder .addOntology( @@ -153,7 +152,7 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { ReadClassInfoV2Builder .builder(subClassIri) .addSuperClass(classIri) - .setDirectCardinalities(cardinalities) + .addProperty(propertyIri.toInternalIri, cardinality) ) ) .build @@ -202,7 +201,7 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { .addClassInfo( ReadClassInfoV2Builder .builder(classIri) - .setDirectCardinalities(OntologyCacheDataBuilder.cardinalitiesMap(propertyIri, ExactlyOne)) + .addProperty(propertyIri.toInternalIri, ExactlyOne) ) ) .build @@ -421,8 +420,6 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { |then this is NOT possible because the new cardinality does violate the |number of times the property is used on each instance """.stripMargin) { - val propertyCardinality = - OntologyCacheDataBuilder.cardinalitiesMap(Anything.Property.hasOtherThing, Unbounded) val data = OntologyCacheDataBuilder.builder .addOntology( ReadOntologyV2Builder @@ -430,7 +427,7 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { .addClassInfo( ReadClassInfoV2Builder .builder(Anything.Class.Thing) - .setDirectCardinalities(propertyCardinality) + .addProperty(Anything.Property.hasOtherThing, Unbounded) ) ) check(cardinalitiesGen(ZeroOrOne, ExactlyOne)) { newCardinality => @@ -477,8 +474,6 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { ), suite("canSetCardinality with deleted object in property reference")( test("given a deleted property was used") { - val propertyCardinality = - OntologyCacheDataBuilder.cardinalitiesMap(Anything.Property.hasOtherThing, Unbounded) val data = OntologyCacheDataBuilder.builder .addOntology( ReadOntologyV2Builder @@ -486,7 +481,7 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { .addClassInfo( ReadClassInfoV2Builder .builder(Anything.Class.Thing) - .setDirectCardinalities(propertyCardinality) + .addProperty(Anything.Property.hasOtherThing, Unbounded) ) ) check(cardinalitiesGen(AtLeastOne)) { newCardinality => @@ -526,8 +521,6 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { |then this is possible because the new cardinality does not violate the |number of times the property is used on the instance """.stripMargin) { - val propertyCardinality = - OntologyCacheDataBuilder.cardinalitiesMap(Anything.Property.hasOtherThing, Unbounded) val data = OntologyCacheDataBuilder.builder .addOntology( ReadOntologyV2Builder @@ -535,7 +528,7 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { .addClassInfo( ReadClassInfoV2Builder .builder(Anything.Class.Thing) - .setDirectCardinalities(propertyCardinality) + .addProperty(Anything.Property.hasOtherThing, Unbounded) ) ) check(cardinalitiesGen(AtLeastOne, ZeroOrOne, ExactlyOne)) { newCardinality => @@ -561,6 +554,51 @@ object CardinalityServiceLiveSpec extends ZIOSpecDefault { | <${Anything.Property.hasOtherThing.value}> false . | |""".stripMargin) + ), + suite("CardinalityServiceLive persistence check")(test(s""" + |Given a three tier subclass hierarchy + |setting required for the middle class property + |should be possible""".stripMargin) { + val ontologyCacheData = OntologyCacheDataBuilder.builder + .addOntology( + ReadOntologyV2Builder + .builder(Biblio.Ontology) + .addClassInfo(ReadClassInfoV2Builder.builder(Biblio.Class.Publication)) + .addClassInfo( + ReadClassInfoV2Builder + .builder(Biblio.Class.Article) + .addSuperClass(Biblio.Class.Publication) + .addProperty(Biblio.Property.hasTitle, Unbounded) + ) + .addClassInfo( + ReadClassInfoV2Builder + .builder(Biblio.Class.JournalArticle) + .addSuperClass(Biblio.Class.Article) + .addProperty(Biblio.Property.hasTitle, AtLeastOne) + ) + ) + .build + for { + _ <- OntologyCacheFake.set(ontologyCacheData) + result <- CardinalityService.canSetCardinality(Biblio.Class.Article, Biblio.Property.hasTitle, AtLeastOne) + } yield assertTrue(result.isRight) + }).provide( + commonLayers, + datasetLayerFromTurtle(s""" + |@prefix rdf: . + |@prefix rdfs: . + | + | + | a <${Biblio.Class.Publication.value}> . + | + | + | a <${Biblio.Class.Article.value}> ; + | <${Biblio.Property.hasTitle.value}> "article title" . + | + | + | a <${Biblio.Class.JournalArticle.value}> ; + | <${Biblio.Property.hasTitle.value}> "journal article title" . + |""".stripMargin) ) ) } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/OntologyCacheDataBuilder.scala b/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/OntologyCacheDataBuilder.scala index c3986906c1..cfbc2c3e00 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/OntologyCacheDataBuilder.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/OntologyCacheDataBuilder.scala @@ -15,7 +15,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.OntologyMetadataV import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.KnoraCardinalityInfo import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 -import org.knora.webapi.slice.ontology.domain.ReadClassInfoV2Builder.ClassInfoContentV2Builder.BuilderClassInfoContentV2Builder import org.knora.webapi.slice.ontology.domain.SmartIriConversion.BetterSmartIri import org.knora.webapi.slice.ontology.domain.SmartIriConversion.BetterSmartIriKeyMap import org.knora.webapi.slice.ontology.domain.SmartIriConversion.TestSmartIriFromInternalIri @@ -116,9 +115,6 @@ object ReadClassInfoV2Builder { def setEntityInfoContent(builder: ClassInfoContentV2Builder.Builder): Builder = setEntityInfoContent(builder.build) - def setDirectCardinalities(c: Map[SmartIri, KnoraCardinalityInfo]): Builder = - setEntityInfoContent(rci.entityInfoContent.toBuilder.setDirectCardinalities(c)) - def setInheritedCardinalities(c: Map[SmartIri, KnoraCardinalityInfo]): Builder = copy(rci = rci.copy(inheritedCardinalities = c.internal)) @@ -127,6 +123,15 @@ object ReadClassInfoV2Builder { def addSuperClass(classIri: SmartIri): Builder = copy(rci = rci.copy(allBaseClasses = rci.allBaseClasses.prepended(classIri))) + + def addProperty(propertyIri: InternalIri, cardinality: Cardinality): Builder = + copy(rci = + rci.copy(entityInfoContent = + rci.entityInfoContent.copy(directCardinalities = + rci.entityInfoContent.directCardinalities + (propertyIri.smartIri -> KnoraCardinalityInfo(cardinality)) + ) + ) + ) } implicit class BuilderReadClassInfoV2(rci: ReadClassInfoV2) { @@ -143,9 +148,6 @@ object ReadClassInfoV2Builder { case class Builder(cic: ClassInfoContentV2) { def build: ClassInfoContentV2 = cic - - def setDirectCardinalities(c: Map[SmartIri, KnoraCardinalityInfo]): Builder = - copy(cic = cic.copy(directCardinalities = c.internal)) } implicit class BuilderClassInfoContentV2Builder(cic: ClassInfoContentV2) {