diff --git a/test_data/all_data/books-data.ttl b/test_data/all_data/books-data.ttl new file mode 100644 index 0000000000..4c044db029 --- /dev/null +++ b/test_data/all_data/books-data.ttl @@ -0,0 +1,57 @@ +@prefix xml: . +@prefix xsd: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix foaf: . +@prefix knora-base: . +@prefix knora-admin: . +@prefix salsah-gui: . +@prefix books: . + + + + a knora-base:TextValue ; + knora-base:valueHasUUID "OTAzYmIzOTktYzA4Ni00NmM0LWFlZGItYzIyNzcyN2FkNjFk"^^xsd:string ; + knora-base:isDeleted false ; + knora-base:valueCreationDate "2018-05-28T15:52:03.897Z"^^xsd:dateTime ; + knora-base:valueHasOrder 0 ; + knora-base:valueHasString "Handbuch Projektmanagement" ; + knora-base:hasPermissions + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser" ; + knora-base:attachedToUser . + + + + a books:Book ; + knora-base:attachedToUser ; + knora-base:attachedToProject ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember" ; + knora-base:creationDate "2019-11-29T10:00:00.673298Z"^^xsd:dateTime ; + books:hasTitle ; + rdfs:label "instance of a book" ; + knora-base:isDeleted false . + + + + a knora-base:IntValue ; + knora-base:valueHasUUID "MmI4YTg1YTctZjM4Yy00MzE2LTkwNTgtYzdjODk1NjcwZmFm"^^xsd:string ; + knora-base:isDeleted false ; + knora-base:valueCreationDate "2018-05-28T15:52:03.897Z"^^xsd:dateTime ; + knora-base:valueHasInteger 1 ; + knora-base:valueHasOrder 0 ; + knora-base:valueHasString "1" ; + knora-base:hasPermissions + "CR knora-admin:Creator|M knora-admin:ProjectMember|V knora-admin:KnownUser|RV knora-admin:UnknownUser" ; + knora-base:attachedToUser . + + + + a books:Page ; + knora-base:attachedToUser ; + knora-base:attachedToProject ; + knora-base:hasPermissions "V knora-admin:UnknownUser|M knora-admin:ProjectMember" ; + knora-base:creationDate "2019-11-29T10:00:00.673298Z"^^xsd:dateTime ; + books:hasPageNumber ; + rdfs:label "instance of a page" ; + knora-base:isDeleted false . diff --git a/test_data/ontologies/books-onto.ttl b/test_data/ontologies/books-onto.ttl new file mode 100644 index 0000000000..808bddaa61 --- /dev/null +++ b/test_data/ontologies/books-onto.ttl @@ -0,0 +1,71 @@ +@prefix xml: . +@prefix xsd: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix foaf: . +@prefix knora-base: . +@prefix salsah-gui: . +@base . + +# A bookish ontology, used only for testing Knora. + +@prefix : . + + + rdf:type owl:Ontology ; + rdfs:label "An ontology about books" ; + knora-base:attachedToProject ; + knora-base:lastModificationDate "2012-12-12T12:12:12.12Z"^^xsd:dateTime . + + + +:hasTitle + rdf:type owl:ObjectProperty ; + rdfs:subPropertyOf knora-base:hasValue ; + rdfs:label "Titel"@de, "title"@en ; + knora-base:subjectClassConstraint :Book ; + knora-base:objectClassConstraint knora-base:TextValue ; + salsah-gui:guiElement salsah-gui:SimpleText ; + salsah-gui:guiAttribute "size=80", "maxlength=255" . + + +#:hasPage +# rdf:type owl:ObjectProperty ; +# rdfs:subPropertyOf knora-base:hasLinkTo ; +# rdfs:label "Seite im Buch"@de, "Page in the book"@en ; +# knora-base:subjectClassConstraint :Book ; +# knora-base:objectClassConstraint :Page ; +# salsah-gui:guiElement salsah-gui:Searchbox . +# +# +#:hasPageValue +# rdf:type owl:ObjectProperty ; +# rdfs:subPropertyOf knora-base:hasLinkToValue ; +# rdfs:label "Seite im Buch"@de, "Page in the book"@en ; +# knora-base:objectClassConstraint knora-base:LinkValue . + + +:Book + rdf:type owl:Class ; + rdfs:subClassOf knora-base:Resource; + knora-base:resourceIcon "book.png" ; + rdfs:label "Buch"@de, "Book"@en ; + rdfs:comment """A resource describing a book."""@en . + + +:hasPageNumber + rdf:type owl:ObjectProperty ; + rdfs:subPropertyOf knora-base:hasValue ; + rdfs:label "Seitenzahl"@de, "Page number"@en ; + knora-base:objectClassConstraint knora-base:IntValue ; + salsah-gui:guiElement salsah-gui:Spinbox ; + salsah-gui:guiAttribute "min=0", "max=-1" . + + +:Page + rdf:type owl:Class ; + rdfs:subClassOf knora-base:Resource; + knora-base:resourceIcon "page.png" ; + rdfs:label "Seite"@de, "Page"@en ; + rdfs:comment """A resource describing a page in a book."""@en . 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 9e0966ad18..a224aeeb3b 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 @@ -2640,6 +2640,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon maybeUnescapedNewLinkValuePropertyDef.map { unescapedNewLinkPropertyDef => unescapedNewLinkPropertyDef.propertyIri -> ReadPropertyInfoV2( entityInfoContent = unescapedNewLinkPropertyDef, + isEditable = true, isResourceProp = true, isLinkValueProp = true ) @@ -2665,7 +2666,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon ) // Read the data back from the cache. - response <- getPropertyDefinitionsFromOntologyV2( propertyIris = Set(internalPropertyIri), allLanguages = true, diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala index 209e8e7071..a6a335be76 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala @@ -1678,6 +1678,46 @@ class OntologyV2R2RSpec extends R2RSpec { } } + "add all IRIs to newly created link value property again" in { + val url = URLEncoder.encode(s"${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}", "UTF-8") + Get( + s"/v2/ontologies/allentities/${url}" + ) ~> ontologiesPath ~> check { + val responseStr: String = responseAs[String] + assert(status == StatusCodes.OK, response.toString) + val responseJsonDoc = JsonLDUtil.parseJsonLD(responseStr) + + val graph = responseJsonDoc.body.requireArray("@graph").value + + val hasOtherNothingValue = graph + .filter( + _.asInstanceOf[JsonLDObject] + .value("@id") + .asInstanceOf[JsonLDString] + .value == "http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherNothingValue" + ) + .head + .asInstanceOf[JsonLDObject] + + val iris = hasOtherNothingValue.value.keySet + + val expectedIris = Set( + OntologyConstants.Rdfs.Comment, + OntologyConstants.Rdfs.Label, + OntologyConstants.Rdfs.SubPropertyOf, + OntologyConstants.KnoraApiV2Complex.IsEditable, + OntologyConstants.KnoraApiV2Complex.IsResourceProperty, + OntologyConstants.KnoraApiV2Complex.IsLinkValueProperty, + OntologyConstants.KnoraApiV2Complex.ObjectType, + OntologyConstants.KnoraApiV2Complex.SubjectType, + "@id", + "@type" + ) + + iris should equal(expectedIris) + } + } + "remove the cardinality for the property anything:hasOtherNothing from the class anything:Nothing" in { val params = s"""{ diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/BUILD.bazel index 4a92416543..263d340e59 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/BUILD.bazel @@ -3,6 +3,47 @@ package(default_visibility = ["//visibility:public"]) load("@io_bazel_rules_scala//scala:scala.bzl", "scala_test") load("//third_party:dependencies.bzl", "ALL_WEBAPI_MAIN_DEPENDENCIES", "BASE_TEST_DEPENDENCIES", "BASE_TEST_DEPENDENCIES_WITH_JSON", "BASE_TEST_DEPENDENCIES_WITH_JSON_LD") + +scala_test( + name = "CacheSpec", + size = "small", # 60s + srcs = [ + "CacheSpec.scala", + ], + data = [ + "//knora-ontologies", + "//test_data", + ], + jvm_flags = ["-Dconfig.resource=fuseki.conf"], + resources = [ + "//webapi/scripts:fuseki_repository_config_ttl_template", + "//webapi/src/main/resources", + "//webapi/src/test/resources", + ], + scalacopts = ["-deprecation"], + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi:test_library", + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/feature", + "//webapi/src/main/scala/org/knora/webapi/instrumentation", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/responders/v2/ontology", + "//webapi/src/main/scala/org/knora/webapi/settings", + "//webapi/src/main/scala/org/knora/webapi/store", + "//webapi/src/main/scala/org/knora/webapi/util/cache", + "@maven//:com_typesafe_akka_akka_actor_2_13", + "@maven//:com_typesafe_config", + "@maven//:com_typesafe_scala_logging_scala_logging_2_13", + "@maven//:org_scalactic_scalactic_2_13", + "@maven//:org_scalatest_scalatest_compatible", + "@maven//:org_scalatest_scalatest_core_2_13", + "@maven//:org_scalatest_scalatest_matchers_core_2_13", + "@maven//:org_scalatest_scalatest_shouldmatchers_2_13", + "@maven//:org_scalatest_scalatest_wordspec_2_13", + ], +) + scala_test( name = "DeleteCardinalitiesFromClassSpec", size = "small", # 60s diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CacheSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CacheSpec.scala new file mode 100644 index 0000000000..f34dde40ad --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CacheSpec.scala @@ -0,0 +1,357 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of the DaSCH Service Platform. + * + * The DaSCH Service Platform is free software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public + * License as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * The DaSCH Service Platform is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with the DaSCH Service Platform. If not, see + * . + */ + +package org.knora.webapi.responders.v2.ontology + +import akka.actor.Props +import org.knora.webapi.feature.{FeatureFactoryConfig, KnoraSettingsFeatureFactoryConfig} +import org.knora.webapi.messages.{OntologyConstants, SmartIri, StringFormatter} +import org.knora.webapi.messages.store.triplestoremessages.{ + IriLiteralV2, + RdfDataObject, + SmartIriLiteralV2, + StringLiteralV2 +} +import org.knora.webapi.messages.util.KnoraSystemInstances +import org.knora.webapi.messages.v2.responder.SuccessResponseV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.{ + PredicateInfoV2, + PropertyInfoContentV2, + ReadOntologyV2, + ReadPropertyInfoV2 +} +import org.knora.webapi.settings.KnoraDispatchers +import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM +import org.knora.webapi.store.triplestore.http.HttpTriplestoreConnector +import org.knora.webapi.util.cache.CacheUtil +import org.knora.webapi.{IntegrationSpec, InternalSchema, TestContainerFuseki} + +import java.time.Instant +import scala.concurrent.{Await, Future} +import scala.concurrent.duration._ +import scala.language.postfixOps +import scala.util.{Failure, Success} + +/** + * This spec is used to test [[org.knora.webapi.responders.v2.ontology.Cache]]. + */ +class CacheSpec extends IntegrationSpec(TestContainerFuseki.PortConfig) { + + private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + val additionalTestData = List( + RdfDataObject( + path = "test_data/ontologies/books-onto.ttl", + name = "http://www.knora.org/ontology/0001/books" + ), + RdfDataObject( + path = "test_data/all_data/books-data.ttl", + name = "http://www.knora.org/data/0001/books" + ) + ) + + val defaultFeatureFactoryConfig: FeatureFactoryConfig = new KnoraSettingsFeatureFactoryConfig(settings) + + // start fuseki http connector actor + private val fusekiActor = system.actorOf( + Props(new HttpTriplestoreConnector()).withDispatcher(KnoraDispatchers.KnoraActorDispatcher), + name = "httpTriplestoreConnector" + ) + + override def beforeAll(): Unit = { + CacheUtil.createCaches(settings.caches) + waitForReadyTriplestore(fusekiActor) + loadTestData(fusekiActor, additionalTestData) + } + + "The basic functionality of the ontology cache" should { + + "successfully load all ontologies" in { + val ontologiesFromCacheFuture: Future[Map[SmartIri, ReadOntologyV2]] = for { + _ <- Cache.loadOntologies( + settings, + fusekiActor, + defaultFeatureFactoryConfig, + KnoraSystemInstances.Users.SystemUser + ) + cacheData: Cache.OntologyCacheData <- Cache.getCacheData + ontologies: Map[SmartIri, ReadOntologyV2] = cacheData.ontologies + } yield ontologies + + ontologiesFromCacheFuture map { res: Map[SmartIri, ReadOntologyV2] => + res.size should equal(13) + } + } + + } + + "Updating the ontology cache," when { + + "removing a property from an ontology," should { + + "remove the property from the cache." in { + val iri: SmartIri = stringFormatter.toSmartIri(additionalTestData.head.name) + val hasTitlePropertyIri = stringFormatter.toSmartIri(s"${additionalTestData.head.name}#hasTitle") + + val previousCacheDataFuture = Cache.getCacheData + val previousCacheData = Await.result(previousCacheDataFuture, 2 seconds) + + val previousBooksMaybe = previousCacheData.ontologies.get(iri) + previousBooksMaybe match { + case Some(previousBooks) => { + // copy books-onto but remove :hasTitle property + val newBooks = previousBooks.copy( + ontologyMetadata = previousBooks.ontologyMetadata.copy( + lastModificationDate = Some(Instant.now()) + ), + properties = previousBooks.properties.view.filterKeys(_ != hasTitlePropertyIri).toMap + ) + + // store new ontology to cache + val newCacheData = previousCacheData.copy( + ontologies = previousCacheData.ontologies + (iri -> newBooks) + ) + Cache.storeCacheData(newCacheData) + + // read back the cache + val newCachedCacheDataFuture = for { + cacheData <- Cache.getCacheData + } yield cacheData + val newCachedCacheData = Await.result(newCachedCacheDataFuture, 2 seconds) + + // ensure that the cache updated correctly + val newCachedBooksMaybe = newCachedCacheData.ontologies.get(iri) + newCachedBooksMaybe match { + case Some(newCachedBooks) => { + // check length + assert(newCachedBooks.properties.size != previousBooks.properties.size) + assert(newCachedBooks.properties.size == newBooks.properties.size) + + // check actual property + previousBooks.properties should contain key (hasTitlePropertyIri) + newCachedBooks.properties should not contain key(hasTitlePropertyIri) + } + } + } + } + } + } + + "adding a property to an ontology," should { + + "add a value property to the cache." in { + + val iri: SmartIri = stringFormatter.toSmartIri(additionalTestData.head.name) + val hasDescriptionPropertyIri = stringFormatter.toSmartIri(s"${additionalTestData.head.name}#hasDescription") + + val previousCacheDataFuture = Cache.getCacheData + val previousCacheData = Await.result(previousCacheDataFuture, 2 seconds) + + val previousBooksMaybe = previousCacheData.ontologies.get(iri) + previousBooksMaybe match { + case Some(previousBooks) => { + // copy books-onto but add :hasDescription property + val descriptionProp = ReadPropertyInfoV2( + entityInfoContent = PropertyInfoContentV2( + propertyIri = hasDescriptionPropertyIri, + predicates = Map( + stringFormatter.toSmartIri(OntologyConstants.Rdf.Type) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.Rdf.Type), + Seq(SmartIriLiteralV2(stringFormatter.toSmartIri(OntologyConstants.Owl.ObjectProperty))) + ), + stringFormatter.toSmartIri(OntologyConstants.Rdfs.Label) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.Rdfs.Label), + Seq( + StringLiteralV2("A Description", language = Some("en")), + StringLiteralV2("Eine Beschreibung", language = Some("de")) + ) + ), + stringFormatter.toSmartIri(OntologyConstants.KnoraBase.SubjectClassConstraint) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.KnoraBase.SubjectClassConstraint), + Seq(SmartIriLiteralV2(iri)) + ), + stringFormatter.toSmartIri(OntologyConstants.KnoraBase.ObjectClassConstraint) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.KnoraBase.ObjectClassConstraint), + Seq(SmartIriLiteralV2(stringFormatter.toSmartIri(OntologyConstants.KnoraBase.TextValue))) + ), + stringFormatter.toSmartIri(OntologyConstants.SalsahGui.GuiElementClass) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.SalsahGui.GuiElementClass), + Seq(SmartIriLiteralV2(stringFormatter.toSmartIri(OntologyConstants.SalsahGui.SimpleText))) + ), + stringFormatter.toSmartIri(OntologyConstants.SalsahGui.GuiAttribute) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.SalsahGui.GuiAttribute), + Seq( + StringLiteralV2("size=80"), + StringLiteralV2("maxlength=255") + ) + ) + ), + subPropertyOf = Set(stringFormatter.toSmartIri(OntologyConstants.KnoraBase.HasValue)), + ontologySchema = InternalSchema + ), + isResourceProp = true, + isEditable = true + ) + val newProps = previousBooks.properties + (hasDescriptionPropertyIri -> descriptionProp) + val newBooks = previousBooks.copy( + ontologyMetadata = previousBooks.ontologyMetadata.copy( + lastModificationDate = Some(Instant.now()) + ), + properties = newProps + ) + + // store new ontology to cache + val newCacheData = previousCacheData.copy( + ontologies = previousCacheData.ontologies + (iri -> newBooks) + ) + Cache.storeCacheData(newCacheData) + + // read back the cache + val newCachedCacheDataFuture = for { + cacheData <- Cache.getCacheData + } yield cacheData + val newCachedCacheData = Await.result(newCachedCacheDataFuture, 2 seconds) + + // ensure that the cache updated correctly + val newCachedBooksMaybe = newCachedCacheData.ontologies.get(iri) + newCachedBooksMaybe match { + case Some(newCachedBooks) => { + // check length + assert(newCachedBooks.properties.size != previousBooks.properties.size) + assert(newCachedBooks.properties.size == newBooks.properties.size) + + // check actual property + previousBooks.properties should not contain key(hasDescriptionPropertyIri) + newCachedBooks.properties should contain key (hasDescriptionPropertyIri) + } + } + } + } + + } + + "add a link property and a link value property to the cache." in { + + val ontologyIri = stringFormatter.toSmartIri("http://www.knora.org/ontology/0001/books") + val hasPagePropertyIri = stringFormatter.toSmartIri("http://www.knora.org/ontology/0001/books#hasPage") + val pagePropertyIri = stringFormatter.toSmartIri("http://www.knora.org/ontology/0001/books#Page") + val hasPageValuePropertyIri = + stringFormatter.toSmartIri("http://www.knora.org/ontology/0001/books#hasPageValue") + val bookIri = stringFormatter.toSmartIri("http://rdfh.ch/0001/book-instance-01") + + val previousCacheData = Await.result(Cache.getCacheData, 2 seconds) + previousCacheData.ontologies.get(ontologyIri) match { + case Some(previousBooks) => { + // copy books-ontology but add link from book to page + val linkPropertyInfoContent = PropertyInfoContentV2( + propertyIri = hasPagePropertyIri, + predicates = Map( + stringFormatter.toSmartIri(OntologyConstants.Rdf.Type) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.Rdf.Type), + Seq(SmartIriLiteralV2(stringFormatter.toSmartIri(OntologyConstants.Owl.ObjectProperty))) + ), + stringFormatter.toSmartIri(OntologyConstants.Rdfs.Label) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.Rdfs.Label), + Seq( + StringLiteralV2("Seite im Buch", language = Some("de")), + StringLiteralV2("Page in the book", language = Some("en")) + ) + ), + stringFormatter.toSmartIri(OntologyConstants.KnoraBase.SubjectClassConstraint) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.KnoraBase.SubjectClassConstraint), + Seq(SmartIriLiteralV2(bookIri)) + ), + stringFormatter.toSmartIri(OntologyConstants.KnoraBase.ObjectClassConstraint) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.KnoraBase.ObjectClassConstraint), + Seq(SmartIriLiteralV2(pagePropertyIri)) + ), + stringFormatter.toSmartIri(OntologyConstants.SalsahGui.GuiElementClass) -> PredicateInfoV2( + predicateIri = stringFormatter.toSmartIri(OntologyConstants.SalsahGui.GuiElementClass), + Seq(SmartIriLiteralV2(stringFormatter.toSmartIri(OntologyConstants.SalsahGui.Searchbox))) + ) + ), + subPropertyOf = Set(stringFormatter.toSmartIri(OntologyConstants.KnoraBase.HasLinkTo)), + ontologySchema = InternalSchema + ) + val hasPageProperties = ReadPropertyInfoV2( + entityInfoContent = linkPropertyInfoContent, + isResourceProp = true, + isEditable = true, + isLinkProp = true + ) + val hasPageValueProperties = ReadPropertyInfoV2( + entityInfoContent = OntologyHelpers.linkPropertyDefToLinkValuePropertyDef(linkPropertyInfoContent), + isResourceProp = true, + isEditable = true, + isLinkValueProp = true + ) + val newProps = previousBooks.properties + + (hasPagePropertyIri -> hasPageProperties) + + (hasPageValuePropertyIri -> hasPageValueProperties) + val newBooks = previousBooks.copy( + ontologyMetadata = previousBooks.ontologyMetadata.copy( + lastModificationDate = Some(Instant.now()) + ), + properties = newProps + ) + + // store new ontology to cache + val newCacheData = previousCacheData.copy( + ontologies = previousCacheData.ontologies + (ontologyIri -> newBooks) + ) + Cache.storeCacheData(newCacheData) + + // read back the cache + val newCachedCacheData = Await.result(Cache.getCacheData, 2 seconds) + + // ensure that the cache updated correctly + newCachedCacheData.ontologies.get(ontologyIri) match { + case Some(newCachedBooks) => { + // check length + assert(newCachedBooks.properties.size != previousBooks.properties.size) + assert(newCachedBooks.properties.size == newBooks.properties.size) + + // check actual property + previousBooks.properties should not contain key(hasPagePropertyIri) + previousBooks.properties should not contain key(hasPageValuePropertyIri) + newCachedBooks.properties should contain key (hasPagePropertyIri) + newCachedBooks.properties should contain key (hasPageValuePropertyIri) + + // check isEditable == true + val newHasPageValuePropertyMaybe = newCachedBooks.properties.get(hasPageValuePropertyIri) + newHasPageValuePropertyMaybe should not equal (None) + newHasPageValuePropertyMaybe match { + case Some(newHasPageValueProperty) => { + assert(newHasPageValueProperty.isEditable) + assert(newHasPageValueProperty.isLinkValueProp) + } + } + + newCachedBooks should equal(newBooks) + } + } + } + } + + } + } + + } +}