From 27b5c866f29795f49dbf584ca20245e90d210d13 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 14 Jan 2022 11:38:44 +0100 Subject: [PATCH] fix(ontology): DSP-API creates wrong partOfValue property (DEV-216) (#1978) * Create test for partOf property * add partOfValue to the creation of value properties * remove unused code * add documentation about partOf and seqnum * rename test description --- docs/02-knora-ontologies/knora-base.md | 19 +- .../webapi/messages/OntologyConstants.scala | 2 + .../v2/ontology/OntologyHelpers.scala | 10 +- .../v2/OntologyResponderV2Spec.scala | 177 ++++++++++++++++++ 4 files changed, 205 insertions(+), 3 deletions(-) diff --git a/docs/02-knora-ontologies/knora-base.md b/docs/02-knora-ontologies/knora-base.md index 77ad403b8a..d0c47814ff 100644 --- a/docs/02-knora-ontologies/knora-base.md +++ b/docs/02-knora-ontologies/knora-base.md @@ -569,8 +569,23 @@ containing metadata about the link. We can visualise the result as the following ![Figure 2](knora-base-fig2.dot.png "Figure 2") Knora allows a user to see a link if the requesting user has permission to see the source and target resources as well -as the -`kb:LinkValue`. +as the `kb:LinkValue`. + +### Part-of (part-whole) relation between resources +A special case of linked resources are _part-of related resources_, i.e. a resource consisting of several other resources. +In order to create a part-of relation between two resources, the resource that is part of another resource needs to have +a property that is a subproperty of `kb:isPartOf`. This property needs to point to the resource class it is part of via +its predicate `knora-api:objectType`. +`kb:isPartOf` itself is a subproperty of `kb:hasLinkTo`. Same as described above for link properties, a corresponding +part-of value property is created automatically. This value property has the same name as the part-of property with +`Value` appended. For example, if in an ontology `data` a property `data:partOf` was defined, the corresponding value +property would be named `data:partOfValue`. This newly created property `data:partOfValue` is defined as a subproperty +of `kb:isPartOfValue`. + +Part-of relations are recommended for resources of type `StillImageRepresentation`. In that case, the resource that is +part of another resource needs to have a property that is a subproperty of `knora-api:seqnum` with an integer as value. +A client can then use this information to leaf through the parts of the compound resource (p.ex. to leaf through the +pages of a book like in [this](https://docs.dasch.swiss/DSP-API/01-introduction/example-project/#resource-classes) example). ### Text with Standoff Markup diff --git a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala index 70a10d4ca8..f23cabfb4e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala @@ -215,6 +215,8 @@ object OntologyConstants { val LinkObj: IRI = KnoraBasePrefixExpansion + "LinkObj" val HasLinkTo: IRI = KnoraBasePrefixExpansion + "hasLinkTo" val HasLinkToValue: IRI = KnoraBasePrefixExpansion + "hasLinkToValue" + val IsPartOf: IRI = KnoraBasePrefixExpansion + "isPartOf" + val IsPartOfValue: IRI = KnoraBasePrefixExpansion + "isPartOfValue" val Region: IRI = KnoraBasePrefixExpansion + "Region" val IsRegionOf: IRI = KnoraBasePrefixExpansion + "isRegionOf" diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala index 4633c7bd4c..401b351246 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala @@ -1950,10 +1950,18 @@ object OntologyHelpers { objects = Seq(SmartIriLiteralV2(OntologyConstants.KnoraBase.LinkValue.toSmartIri)) )) + val subPropertyOf: SmartIri = internalPropertyDef.subPropertyOf match { + case subProps if subProps.contains(OntologyConstants.KnoraBase.IsPartOf.toSmartIri) => + OntologyConstants.KnoraBase.IsPartOfValue.toSmartIri + case subProps if subProps.contains(OntologyConstants.KnoraBase.HasLinkTo.toSmartIri) => + OntologyConstants.KnoraBase.HasLinkToValue.toSmartIri + case _ => OntologyConstants.KnoraBase.HasLinkToValue.toSmartIri + } + internalPropertyDef.copy( propertyIri = linkValuePropIri, predicates = newPredicates, - subPropertyOf = Set(OntologyConstants.KnoraBase.HasLinkToValue.toSmartIri) + subPropertyOf = Set(subPropertyOf) ) } 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 1ea0626b27..37c8ce82bb 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 @@ -2366,6 +2366,183 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { } } + "create classes anything:wholeThing and anything:partThing with a isPartOf relation and its corresponding value property" in { + + // Create class partThing + + val partThingClassIri = AnythingOntologyIri.makeEntityIri("partThing") + + val partThingClassInfoContent = ClassInfoContentV2( + classIri = partThingClassIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri)) + ), + OntologyConstants.Rdfs.Label.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, + objects = Seq(StringLiteralV2("Thing as part", Some("en"))) + ), + OntologyConstants.Rdfs.Comment.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Comment.toSmartIri, + objects = Seq(StringLiteralV2("Thing that is part of something else", Some("en"))) + ) + ), + subClassOf = Set(OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri), + ontologySchema = ApiV2Complex + ) + + responderManager ! CreateClassRequestV2( + classInfoContent = partThingClassInfoContent, + 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") + ) + anythingLastModDate = newAnythingLastModDate + } + + // Create class wholeThing + + val wholeThingClassIri = AnythingOntologyIri.makeEntityIri("wholeThing") + + val wholeThingClassInfoContent = ClassInfoContentV2( + classIri = wholeThingClassIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri)) + ), + OntologyConstants.Rdfs.Label.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, + objects = Seq(StringLiteralV2("Thing as a whole", Some("en"))) + ), + OntologyConstants.Rdfs.Comment.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Comment.toSmartIri, + objects = Seq(StringLiteralV2("A thing that has multiple parts", Some("en"))) + ) + ), + subClassOf = Set(OntologyConstants.KnoraApiV2Complex.Resource.toSmartIri), + ontologySchema = ApiV2Complex + ) + + responderManager ! CreateClassRequestV2( + classInfoContent = wholeThingClassInfoContent, + 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") + ) + anythingLastModDate = newAnythingLastModDate + } + + // Create property partOf with subject partThing and object wholeThing + + val partOfPropertyIri = AnythingOntologyIri.makeEntityIri("partOf") + + val partOfPropertyInfoContent = PropertyInfoContentV2( + propertyIri = partOfPropertyIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.ObjectProperty.toSmartIri)) + ), + OntologyConstants.KnoraApiV2Complex.SubjectType.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.KnoraApiV2Complex.SubjectType.toSmartIri, + objects = Seq(SmartIriLiteralV2(AnythingOntologyIri.makeEntityIri("partThing"))) + ), + OntologyConstants.KnoraApiV2Complex.ObjectType.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.KnoraApiV2Complex.ObjectType.toSmartIri, + objects = Seq(SmartIriLiteralV2(AnythingOntologyIri.makeEntityIri("wholeThing"))) + ), + OntologyConstants.Rdfs.Label.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, + objects = Seq( + StringLiteralV2("is part of", Some("en")), + StringLiteralV2("ist Teil von", Some("de")) + ) + ), + OntologyConstants.Rdfs.Comment.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Comment.toSmartIri, + objects = Seq( + StringLiteralV2("Represents a part of a whole relation", Some("en")), + StringLiteralV2("Repräsentiert eine Teil-Ganzes-Beziehung", Some("de")) + ) + ), + OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.SalsahGuiApiV2WithValueObjects.GuiElementProp.toSmartIri, + objects = Seq(SmartIriLiteralV2("http://api.knora.org/ontology/salsah-gui/v2#Searchbox".toSmartIri)) + ) + ), + subPropertyOf = Set(OntologyConstants.KnoraBase.IsPartOf.toSmartIri), + ontologySchema = ApiV2Complex + ) + + responderManager ! CreatePropertyRequestV2( + propertyInfoContent = partOfPropertyInfoContent, + lastModificationDate = anythingLastModDate, + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) + + expectMsgPF(timeout) { case msg: ReadOntologyV2 => + val externalOntology = msg.toOntologySchema(ApiV2Complex) + assert(externalOntology.properties.size == 1) + val property = externalOntology.properties(partOfPropertyIri) + // check that partOf is a subproperty of knora-api:isPartOf + property.entityInfoContent.subPropertyOf.contains( + OntologyConstants.KnoraApiV2Complex.IsPartOf.toSmartIri + ) should ===(true) + 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 + } + + // Check that the corresponding partOfValue was created + val partOfValuePropertyIri = AnythingOntologyIri.makeEntityIri("partOfValue") + + val partOfValuePropGetRequest = PropertiesGetRequestV2( + propertyIris = Set(partOfValuePropertyIri), + allLanguages = true, + requestingUser = anythingAdminUser + ) + + responderManager ! partOfValuePropGetRequest + + expectMsgPF(timeout) { case msg: ReadOntologyV2 => + val externalOntology = msg.toOntologySchema(ApiV2Complex) + assert(externalOntology.properties.size == 1) + val property = externalOntology.properties(partOfValuePropertyIri) + // check that partOfValue is a subproperty of knora-api:isPartOfValue + property.entityInfoContent.subPropertyOf.contains( + OntologyConstants.KnoraApiV2Complex.IsPartOfValue.toSmartIri + ) should ===(true) + val metadata = externalOntology.ontologyMetadata + val newAnythingLastModDate = metadata.lastModificationDate.getOrElse( + throw AssertionException(s"${metadata.ontologyIri} has no last modification date") + ) + anythingLastModDate = newAnythingLastModDate + } + } + "change the metadata of the 'anything' ontology" in { val newLabel = "The modified anything ontology"