From 12e6c64ace2776529619ab4cdbf00c6dbfd7afd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinbo=CC=88lting?= Date: Fri, 23 Dec 2022 10:50:51 +0100 Subject: [PATCH] Move knora-ontologies to webapi/src/main/resources and keep symbolic link to folder in project root Remove unused parameter `appConfig` from org.knora.webapi.util.ActorUtil#zio2Message cleanup code: remove unused code, simplify Revert "Move knora-ontologies to webapi/src/main/resources and keep symbolic link to folder in project root" This reverts commit 48f5e93b7e443a6867baa257db2a076e1df202b6. fmt Fix warnings in CardinalityHandler Overload case class constructor and simplify SparqlAskRequest construction Use SmartIri type in query fix scaladoc use InternalIri as parameter use InternalIri in CardinalitiesSpec wip // add isPropertyUsedInResources to CardinalityService wip // add TriplestoreServiceFake wip // add CardinalityService wip // add CardinalityService wip // add CardinalityService wip // add canWidenCardinality method to CardinalityService wip // add canWidenCardinality method to CardinalityService wip // add canWidenCardinality method to CardinalityService fmt reorganize TestDatasetBuilder add headers cleanup cleanup cleanup wip // Cardinality model fmt reorganize packages fix compile error and turn Cardinality into sealed trait wip add test wip add failing tests wip add failing tests add OntologyCache and fake implementation add methods to IriConverter add methods to IriConverter add check and tests add check and tests Add headers Update webapi/src/it/scala/org/knora/webapi/slice/resourceinfo/api/IriConverterLiveSpec.scala remove file simplify remove unused code fix test labels fmt fix compile warnings and add type annotation to public field Replace if else with "pattern matching" Replace if else with "pattern matching" Rename isStricterThan method cleanup, remove unused code Remove Option.get by more elaborate pattern matching extract explanation method prevent knora-admin and knora-base ontologies to be change Introduce CanSetCardinalityCheckResult in order to distinguish why setting is not possible Introduce CanSetCardinalityCheckResult in order to distinguish why setting is not possible Introduce CanSetCardinalityCheckResult in order to distinguish why setting is not possible Introduce CanSetCardinalityCheckResult in order to distinguish why setting is not possible fix type in trait Add RestCardinalityService Use RestCardinalityService in OntologiesRouteV2 and assemble layers fmt add headers extend response with optional reason of failure fix test setup fmt Introduce common ReadOnlyRepository and CrudRe traits header && fmt Re-add old behaviour fmt fixup R2R spec Introduce canUpdateCardinality Introduce requestcontext completion with run unsafe zio fmt fix error message make private move getStringQueryParam to RouteUtilV2 move request params checking into RestCardinalityService fmt finetuning fmt --- .../src/main/scala/dsp/errors/Errors.scala | 6 + .../it/scala/org/knora/webapi/R2RSpec.scala | 19 +- .../org/knora/webapi/core/LayersTest.scala | 11 + .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 24 +- .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 5 +- .../v2/ResourcesResponderV2Spec.scala | 2 +- .../v2/ontology/CardinalitiesSpec.scala | 21 +- .../api/IriConverterLiveSpec.scala | 66 +++- .../org/knora/webapi/core/LayersLive.scala | 10 + .../webapi/core/actors/RoutingActor.scala | 6 +- .../webapi/messages/OntologyConstants.scala | 1 + .../webapi/messages/StringFormatter.scala | 42 ++- .../TriplestoreMessages.scala | 5 +- .../v2/responder/KnoraResponseV2.scala | 13 +- .../webapi/responders/ActorToZioBridge.scala | 5 + .../responders/EntityAndClassIriService.scala | 2 +- .../responders/v2/OntologyResponderV2.scala | 39 -- .../v2/ontology/CardinalityHandler.scala | 72 ++-- .../org/knora/webapi/routing/ApiRoutes.scala | 14 +- .../knora/webapi/routing/Authenticator.scala | 8 + .../knora/webapi/routing/RouteUtilV2.scala | 77 ++-- .../org/knora/webapi/routing/RouteUtilZ.scala | 5 + .../knora/webapi/routing/UnsafeZioRun.scala | 19 + .../admin/AuthenticatorServiceLive.scala | 5 + .../webapi/routing/v2/OntologiesRouteV2.scala | 52 ++- .../slice/common/service/Repository.scala | 112 ++++++ .../api/service/RestCardinalityService.scala | 122 +++++++ .../ontology/domain/model/Cardinality.scala | 65 ++++ .../domain/service/CardinalityService.scala | 283 +++++++++++++++ .../domain/service/OntologyRepo.scala | 21 ++ .../ontology/repo/service/OntologyCache.scala | 26 ++ .../repo/service/OntologyRepoLive.scala | 41 +++ .../resourceinfo/domain/InternalIri.scala | 2 +- .../resourceinfo/domain/IriConverter.scala | 19 +- .../org/knora/webapi/util/ActorUtil.scala | 2 - .../queries/sparql/v2/isEntityUsed.scala.txt | 4 +- .../sparql/v2/isPropertyUsed.scala.txt | 50 ++- .../webapi/responders/ActorDepsTest.scala | 26 ++ .../knora/webapi/routing/RouteUtilZSpec.scala | 5 + .../admin/AuthenticatorServiceLiveSpec.scala | 5 + .../domain/CardinalityServiceLiveSpec.scala | 342 ++++++++++++++++++ .../domain/model/CardinalitySpec.scala | 80 ++++ .../ontology/repo/OntologyCacheFakeSpec.scala | 34 ++ .../repo/service/OntologyCacheFake.scala | 36 ++ .../repo/service/OntologyRepoLiveSpec.scala | 86 +++++ .../domain/IriTestConstants.scala | 35 ++ .../triplestore/TestDatasetBuilder.scala | 45 +++ .../api/TriplestoreServiceFake.scala | 147 ++++++++ .../api/TriplestoreServiceFakeSpec.scala | 100 +++++ 49 files changed, 1942 insertions(+), 275 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/common/service/Repository.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/RestCardinalityService.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/model/Cardinality.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/CardinalityService.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCache.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/responders/ActorDepsTest.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/CardinalityServiceLiveSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/model/CardinalitySpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/OntologyCacheFakeSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCacheFake.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLiveSpec.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/domain/IriTestConstants.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/store/triplestore/TestDatasetBuilder.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFake.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFakeSpec.scala diff --git a/dsp-shared/src/main/scala/dsp/errors/Errors.scala b/dsp-shared/src/main/scala/dsp/errors/Errors.scala index 8d1b78a7846..b91f5b8fb81 100644 --- a/dsp-shared/src/main/scala/dsp/errors/Errors.scala +++ b/dsp-shared/src/main/scala/dsp/errors/Errors.scala @@ -85,6 +85,12 @@ object RequestRejectedException { * @param message a description of the error. */ case class BadRequestException(message: String) extends RequestRejectedException(message) +object BadRequestException { + def invalidQueryParamValue(key: String): BadRequestException = + BadRequestException(s"Invalid value for query parameter '$key'") + def missingQueryParamValue(key: String): BadRequestException = + BadRequestException(s"Missing query parameter '$key'") +} /** * An exception indicating that a user has provided bad credentials. diff --git a/webapi/src/it/scala/org/knora/webapi/R2RSpec.scala b/webapi/src/it/scala/org/knora/webapi/R2RSpec.scala index a7a78750206..cfe5c49efd8 100644 --- a/webapi/src/it/scala/org/knora/webapi/R2RSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/R2RSpec.scala @@ -5,6 +5,7 @@ package org.knora.webapi +import akka.actor.ActorRef import akka.http.scaladsl.model.HttpResponse import akka.http.scaladsl.testkit.ScalatestRouteTest import com.typesafe.scalalogging.Logger @@ -13,16 +14,17 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import zio._ import zio.logging.backend.SLF4J - import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.TimeUnit import scala.concurrent.Await import scala.concurrent.Future + import org.knora.webapi.config.AppConfig import org.knora.webapi.core.AppRouter import org.knora.webapi.core.AppServer +import org.knora.webapi.core.LayersTest.DefaultTestEnvironmentWithoutSipi import org.knora.webapi.core.TestStartupUtils import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.util.rdf._ @@ -50,7 +52,8 @@ abstract class R2RSpec * The effect layers from which the App is built. * Can be overriden in specs that need other implementations. */ - lazy val effectLayers = core.LayersTest.integrationTestsWithFusekiTestcontainers(Some(system)) + lazy val effectLayers: ULayer[DefaultTestEnvironmentWithoutSipi] = + core.LayersTest.integrationTestsWithFusekiTestcontainers(Some(system)) /** * `Bootstrap` will ensure that everything is instantiated when the Runtime is created @@ -63,10 +66,7 @@ abstract class R2RSpec ] = ZLayer.empty ++ Runtime.removeDefaultLoggers ++ SLF4J.slf4j ++ effectLayers // create a configured runtime - private val runtime = Unsafe.unsafe { implicit u => - Runtime.unsafe - .fromLayer(bootstrap) - } + val runtime: Runtime.Scoped[Environment] = Unsafe.unsafe(implicit u => Runtime.unsafe.fromLayer(bootstrap)) // An effect for getting stuff out, so that we can pass them // to some legacy code @@ -90,11 +90,11 @@ abstract class R2RSpec // main difference to other specs (no own systen and executionContext defined) lazy val rdfDataObjects = List.empty[RdfDataObject] val log: Logger = Logger(this.getClass()) - val appActor = router.ref + val appActor: ActorRef = router.ref // needed by some tests - val routeData = KnoraRouteData(system, appActor, config) - val appConfig = config + val routeData: KnoraRouteData = KnoraRouteData(system, appActor, config) + val appConfig: AppConfig = config final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ @@ -110,7 +110,6 @@ abstract class R2RSpec } final override def afterAll(): Unit = { - /* Stop ZIO runtime and release resources (e.g., running docker containers) */ Unsafe.unsafe { implicit u => runtime.unsafe.shutdown() diff --git a/webapi/src/it/scala/org/knora/webapi/core/LayersTest.scala b/webapi/src/it/scala/org/knora/webapi/core/LayersTest.scala index 87aeb665a85..9c33ea46497 100644 --- a/webapi/src/it/scala/org/knora/webapi/core/LayersTest.scala +++ b/webapi/src/it/scala/org/knora/webapi/core/LayersTest.scala @@ -7,7 +7,12 @@ import org.knora.webapi.auth.JWTService import org.knora.webapi.config.AppConfig import org.knora.webapi.config.AppConfigForTestContainers import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.responders.ActorDeps import org.knora.webapi.routing.ApiRoutes +import org.knora.webapi.slice.ontology.api.service.RestCardinalityService +import org.knora.webapi.slice.ontology.domain.service.CardinalityService +import org.knora.webapi.slice.ontology.repo.service.OntologyCache +import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.slice.resourceinfo.domain.ResourceInfoRepo @@ -44,6 +49,7 @@ object LayersTest { with IriConverter with RepositoryUpdater with ResourceInfoRepo + with RestCardinalityService with RestResourceInfoService with State with StringFormatter @@ -53,15 +59,20 @@ object LayersTest { private val commonLayersForAllIntegrationTests = ZLayer.makeSome[CommonR0, CommonR]( + ActorDeps.layer, ApiRoutes.layer, AppRouter.layer, CacheServiceInMemImpl.layer, CacheServiceManager.layer, + CardinalityService.layer, HttpServer.layer, IIIFServiceManager.layer, IriConverter.layer, + OntologyCache.layer, + OntologyRepoLive.layer, RepositoryUpdater.layer, ResourceInfoRepo.layer, + RestCardinalityService.layer, RestResourceInfoService.layer, State.layer, StringFormatter.test, diff --git a/webapi/src/it/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala b/webapi/src/it/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala index cd7faa8d614..a4dab508543 100644 --- a/webapi/src/it/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala @@ -64,9 +64,9 @@ class OntologyV2R2RSpec extends R2RSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance private val ontologiesPath = - DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData, runtime).makeRoute) private val resourcesPath = - DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, null).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, runtime).makeRoute) implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration @@ -1010,7 +1010,7 @@ class OntologyV2R2RSpec extends R2RSpec { val expectedResponse: String = s"""{ | "knora-api:lastModificationDate": { - | "@value": "${newFreetestLastModDate}", + | "@value": "$newFreetestLastModDate", | "@type": "xsd:dateTimeStamp" | }, | "rdfs:label": "freetest", @@ -1092,7 +1092,7 @@ class OntologyV2R2RSpec extends R2RSpec { val expectedResponse: String = s"""{ | "knora-api:lastModificationDate": { - | "@value": "${newFreetestLastModDate}", + | "@value": "$newFreetestLastModDate", | "@type": "xsd:dateTimeStamp" | }, | "rdfs:label": "freetest", @@ -1718,7 +1718,7 @@ 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}" + s"/v2/ontologies/allentities/$url" ) ~> ontologiesPath ~> check { val responseStr: String = responseAs[String] assert(status == StatusCodes.OK, response.toString) @@ -1754,7 +1754,7 @@ class OntologyV2R2RSpec extends R2RSpec { iris should equal(expectedIris) val isEditable = hasOtherNothingValue.requireBoolean(OntologyConstants.KnoraApiV2Complex.IsEditable) - isEditable shouldBe (true) + isEditable shouldBe true } } @@ -1816,7 +1816,7 @@ class OntologyV2R2RSpec extends R2RSpec { // load back the ontology to verify that the updated property still is editable val encodedIri = URLEncoder.encode(s"${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}", "UTF-8") Get( - s"/v2/ontologies/allentities/${encodedIri}" + s"/v2/ontologies/allentities/$encodedIri" ) ~> ontologiesPath ~> check { val responseStr: String = responseAs[String] assert(status == StatusCodes.OK, response.toString) @@ -1889,7 +1889,7 @@ class OntologyV2R2RSpec extends R2RSpec { // load back the ontology to verify that the updated property still is editable val encodedIri = URLEncoder.encode(s"${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}", "UTF-8") Get( - s"/v2/ontologies/allentities/${encodedIri}" + s"/v2/ontologies/allentities/$encodedIri" ) ~> ontologiesPath ~> check { val responseStr: String = responseAs[String] assert(status == StatusCodes.OK, response.toString) @@ -1935,7 +1935,7 @@ class OntologyV2R2RSpec extends R2RSpec { // load back the ontology to verify that the updated property still is editable val encodedIri = URLEncoder.encode(s"${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}", "UTF-8") Get( - s"/v2/ontologies/allentities/${encodedIri}" + s"/v2/ontologies/allentities/$encodedIri" ) ~> ontologiesPath ~> check { val responseStr: String = responseAs[String] assert(status == StatusCodes.OK, response.toString) @@ -2008,7 +2008,7 @@ class OntologyV2R2RSpec extends R2RSpec { // load back the ontology to verify that the updated property still is editable val encodedIri = URLEncoder.encode(s"${SharedOntologyTestDataADM.ANYTHING_ONTOLOGY_IRI_LocalHost}", "UTF-8") Get( - s"/v2/ontologies/allentities/${encodedIri}" + s"/v2/ontologies/allentities/$encodedIri" ) ~> ontologiesPath ~> check { val responseStr: String = responseAs[String] assert(status == StatusCodes.OK, response.toString) @@ -3712,7 +3712,7 @@ class OntologyV2R2RSpec extends R2RSpec { // check the ontology to see if all worked as it should val url = URLEncoder.encode(s"http://0.0.0.0:3333/ontology/0001/freetest/v2", "UTF-8") Get( - s"/v2/ontologies/allentities/${url}" + s"/v2/ontologies/allentities/$url" ) ~> ontologiesPath ~> check { val responseStr: String = responseAs[String] assert(status == StatusCodes.OK, response.toString) @@ -3845,7 +3845,7 @@ class OntologyV2R2RSpec extends R2RSpec { // check the ontology to see if all worked as it should val url = URLEncoder.encode(s"http://0.0.0.0:3333/ontology/0001/freetest/v2", "UTF-8") Get( - s"/v2/ontologies/allentities/${url}" + s"/v2/ontologies/allentities/$url" ) ~> ontologiesPath ~> check { val responseStr: String = responseAs[String] assert(status == StatusCodes.OK, response.toString) diff --git a/webapi/src/it/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/it/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index eb98d92569e..f2a79a3d1e9 100644 --- a/webapi/src/it/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -21,15 +21,14 @@ import org.xmlunit.builder.Input import org.xmlunit.diff.Diff import spray.json.JsValue import spray.json.JsonParser - import java.net.URLEncoder import java.nio.file.Paths import java.time.Instant import scala.collection.mutable.ArrayBuffer import scala.concurrent.Await import scala.concurrent.duration._ - import dsp.errors.AssertionException + import org.knora.webapi._ import org.knora.webapi.e2e.ClientTestDataCollector import org.knora.webapi.e2e.InstanceChecker @@ -2113,7 +2112,7 @@ class ResourcesRouteV2E2ESpec extends E2ESpec { "correctly update the ontology cache when adding a resource, so that the resource can afterwards be found by gravsearch" in { val freetestLastModDate: Instant = Instant.parse("2012-12-12T12:12:12.12Z") - DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData, runtime).makeRoute) val auth = BasicHttpCredentials(SharedTestDataADM.anythingAdminUser.email, SharedTestDataADM.testPass) // create a new resource class and add a property with cardinality to it diff --git a/webapi/src/it/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/webapi/src/it/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index adf4f735a75..21e8c72d1cd 100644 --- a/webapi/src/it/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/webapi/src/it/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -2324,7 +2324,7 @@ class ResourcesResponderV2Spec extends CoreSpec with ImplicitSender { val isEntityUsedSparql: String = org.knora.webapi.messages.twirl.queries.sparql.v2.txt .isEntityUsed( - entityIri = resourceIriToErase.get.toSmartIri, + entityIri = resourceIriToErase.get.toSmartIri.toInternalIri, ignoreKnoraConstraints = true ) .toString() diff --git a/webapi/src/it/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala b/webapi/src/it/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala index 60fd5b5069d..a8cacf83349 100644 --- a/webapi/src/it/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala @@ -8,7 +8,6 @@ package org.knora.webapi.responders.v2.ontology import akka.util.Timeout import org.knora.webapi.CoreSpec -import org.knora.webapi.InternalSchema import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter @@ -25,8 +24,8 @@ class CardinalitiesSpec extends CoreSpec { "Cardinalities.isPropertyUsedInResources()" should { "detect that property is in use, when used in a resource" in { - val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasText").toOntologySchema(InternalSchema) - val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toOntologySchema(InternalSchema) + val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasText").toInternalIri + val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toInternalIri println(s"internalPropertyIri: $internalPropertyIri") val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) @@ -34,8 +33,8 @@ class CardinalitiesSpec extends CoreSpec { } "detect that property is not in use, when not used in a resource" in { - val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasText").toOntologySchema(InternalSchema) - val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTestResourceClass").toOntologySchema(InternalSchema) + val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasText").toInternalIri + val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTestResourceClass").toInternalIri println(s"internalPropertyIri: $internalPropertyIri") val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) @@ -45,8 +44,8 @@ class CardinalitiesSpec extends CoreSpec { } "detect that property is not in use, when not used in a resource of that class (even when used in another class)" in { - val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasIntegerProperty").toOntologySchema(InternalSchema) - val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toOntologySchema(InternalSchema) + val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasIntegerProperty").toInternalIri + val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toInternalIri println(s"internalPropertyIri: $internalPropertyIri") val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) @@ -57,8 +56,8 @@ class CardinalitiesSpec extends CoreSpec { "detect that link property is in use, when used in a resource" in { val anythingOntologyIri = "http://0.0.0.0:3333/ontology/0001/anything/v2".toSmartIri - val internalPropertyIri = anythingOntologyIri.makeEntityIri("isPartOfOtherThing").toOntologySchema(InternalSchema) - val internalClassIri = anythingOntologyIri.makeEntityIri("Thing").toOntologySchema(InternalSchema) + val internalPropertyIri = anythingOntologyIri.makeEntityIri("isPartOfOtherThing").toInternalIri + val internalClassIri = anythingOntologyIri.makeEntityIri("Thing").toInternalIri println(s"internalPropertyIri: $internalPropertyIri") val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) @@ -66,8 +65,8 @@ class CardinalitiesSpec extends CoreSpec { } "detect that property is in use, when used in a resource of a subclass" in { - val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasDecimal").toOntologySchema(InternalSchema) - val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toOntologySchema(InternalSchema) + val internalPropertyIri = freetestOntologyIri.makeEntityIri("hasDecimal").toInternalIri + val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toInternalIri println(s"internalPropertyIri: $internalPropertyIri") val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) diff --git a/webapi/src/it/scala/org/knora/webapi/slice/resourceinfo/api/IriConverterLiveSpec.scala b/webapi/src/it/scala/org/knora/webapi/slice/resourceinfo/api/IriConverterLiveSpec.scala index 27a3c20759c..3064056c819 100644 --- a/webapi/src/it/scala/org/knora/webapi/slice/resourceinfo/api/IriConverterLiveSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/slice/resourceinfo/api/IriConverterLiveSpec.scala @@ -1,22 +1,60 @@ package org.knora.webapi.slice.resourceinfo.api import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.slice.resourceinfo.domain -import org.knora.webapi.slice.resourceinfo.domain.{InternalIri, IriConverter} +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resourceinfo.domain.IriConverter import zio.test._ +import org.knora.webapi.IRI + object IriConverterLiveSpec extends ZIOSpecDefault { - def spec = suite("IriConverterLive")( - test("should not convert the projectIri") { - for { - internal <- IriConverter.asInternalIri("http://project-iri") - } yield assertTrue(internal == domain.InternalIri("http://project-iri")) - }, - test("should convert a resourceClassIri") { - for { - internal <- IriConverter.asInternalIri("http://0.0.0.0:3333/ontology/0001/anything/v2#Thing") - } yield assertTrue(internal == InternalIri("http://www.knora.org/ontology/0001/anything#Thing")) - } - ).provide(IriConverter.layer, StringFormatter.test) + private val someInternalIri: IRI = "http://www.knora.org/ontology/0001/anything#Thing" + private val someExternalIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing" + + def spec: Spec[Any, Throwable] = + suite("IriConverter")( + suite("asInternalIri(IRI)")( + test("should not convert an already internal iri") { + for { + actual <- IriConverter.asInternalIri(someInternalIri) + } yield assertTrue(actual == InternalIri(someInternalIri)) + }, + test("should convert an external resourceClassIri to the internal representation") { + for { + actual <- IriConverter.asInternalIri(someExternalIri) + } yield assertTrue(actual == InternalIri(someInternalIri)) + }, + test("should fail if String is no IRI") { + for { + actual <- IriConverter.asInternalIri("notAnIRI").exit + } yield assertTrue(actual.isFailure) + } + ), + suite("asSmartIri(IRI)")( + test("when provided an internal Iri should return correct SmartIri") { + for { + actual <- IriConverter.asSmartIri(someInternalIri) + } yield assertTrue(actual.toIri == someInternalIri) + }, + test("when provided an external Iri should return correct SmartIri") { + for { + actual <- IriConverter.asSmartIri(someExternalIri) + } yield assertTrue(actual.toIri == someExternalIri) + } + ), + suite("asInternalSmartIri(InternalIri)")( + test("should return correct SmartIri") { + for { + actual <- IriConverter.asInternalSmartIri(someInternalIri) + } yield assertTrue(actual.toIri == someInternalIri) + }, + test("should fail if it is an external IRI") { + for { + actual <- + IriConverter.asInternalSmartIri(someExternalIri).exit + } yield assertTrue(actual.isFailure) + } + ) + ).provide(IriConverter.layer, StringFormatter.test) } diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 5cb4c5a5a3d..59339de9860 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -18,6 +18,10 @@ import org.knora.webapi.responders.admin.ProjectsService import org.knora.webapi.routing.ApiRoutes import org.knora.webapi.routing.admin.AuthenticatorService import org.knora.webapi.routing.admin.ProjectsRouteZ +import org.knora.webapi.slice.ontology.api.service.RestCardinalityService +import org.knora.webapi.slice.ontology.domain.service.CardinalityService +import org.knora.webapi.slice.ontology.repo.service.OntologyCache +import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive import org.knora.webapi.slice.resourceinfo.api.ResourceInfoRoute import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService import org.knora.webapi.slice.resourceinfo.domain.IriConverter @@ -50,6 +54,8 @@ object LayersLive { with IIIFService with JWTService with RepositoryUpdater + with RestResourceInfoService + with RestCardinalityService with State with TriplestoreServiceManager with TriplestoreService @@ -69,16 +75,20 @@ object LayersLive { AuthenticatorService.layer, CacheServiceInMemImpl.layer, CacheServiceManager.layer, + CardinalityService.layer, HttpServer.layer, HttpServerZ.layer, // this is the new ZIO HTTP server layer IIIFServiceManager.layer, IIIFServiceSipiImpl.layer, IriConverter.layer, JWTService.layer, + OntologyCache.layer, + OntologyRepoLive.layer, ProjectsRouteZ.layer, RepositoryUpdater.layer, ResourceInfoRepo.layer, ResourceInfoRoute.layer, + RestCardinalityService.layer, RestResourceInfoService.layer, ProjectsService.live, State.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala b/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala index 0a61834c8c1..a625825ef1f 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/actors/RoutingActor.scala @@ -161,10 +161,10 @@ final case class RoutingActor( case sipiResponderRequestADM: SipiResponderRequestADM => ActorUtil.future2Message(sender(), sipiRouterADM.receive(sipiResponderRequestADM), log) case msg: CacheServiceRequest => - ActorUtil.zio2Message(sender(), cacheServiceManager.receive(msg), appConfig, log, runtime) - case msg: IIIFRequest => ActorUtil.zio2Message(sender(), iiifServiceManager.receive(msg), appConfig, log, runtime) + ActorUtil.zio2Message(sender(), cacheServiceManager.receive(msg), log, runtime) + case msg: IIIFRequest => ActorUtil.zio2Message(sender(), iiifServiceManager.receive(msg), log, runtime) case msg: TriplestoreRequest => - ActorUtil.zio2Message(sender(), triplestoreManager.receive(msg), appConfig, log, runtime) + ActorUtil.zio2Message(sender(), triplestoreManager.receive(msg), log, runtime) case other => throw UnexpectedMessageException( 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 2173bda3e48..de516d58f1e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/OntologyConstants.scala @@ -685,6 +685,7 @@ object OntologyConstants { val Result: IRI = KnoraApiV2PrefixExpansion + "result" val Error: IRI = KnoraApiV2PrefixExpansion + "error" val CanDo: IRI = KnoraApiV2PrefixExpansion + "canDo" + val CannotDoReason: IRI = KnoraApiV2PrefixExpansion + "CannotDoReason" val MayHaveMoreResults: IRI = KnoraApiV2PrefixExpansion + "mayHaveMoreResults" val EventType: IRI = KnoraApiV2PrefixExpansion + "eventType" val EventBody: IRI = KnoraApiV2PrefixExpansion + "eventBody" diff --git a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala index 8d7131b792c..5d2676f64cb 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -44,6 +44,7 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1 import org.knora.webapi.messages.v2.responder.KnoraContentV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ +import org.knora.webapi.slice.resourceinfo.domain.InternalIri import org.knora.webapi.util.Base64UrlCheckDigit import org.knora.webapi.util.JavaUtil @@ -358,6 +359,8 @@ sealed trait SmartIri extends Ordered[SmartIri] with KnoraContentV2[SmartIri] { def toIri: IRI = toString + def toInternalIri: InternalIri = InternalIri(toOntologySchema(InternalSchema).toIri) + /** * Returns `true` if this is a Knora data or definition IRI. */ @@ -1084,27 +1087,28 @@ class StringFormatter private ( override def getStandoffStartIndex: Option[Int] = iriInfo.standoffStartIndex - lazy val ontologyFromEntity: SmartIri = if (isKnoraOntologyIri) { - throw DataConversionException(s"$iri is not a Knora entity IRI") - } else { - val lastHashPos = iri.lastIndexOf('#') - - val entityDelimPos = if (lastHashPos >= 0) { - lastHashPos + lazy val ontologyFromEntity: SmartIri = + if (isKnoraOntologyIri) { + throw DataConversionException(s"$iri is not a Knora entity IRI") } else { - val lastSlashPos = iri.lastIndexOf('/') + val lastHashPos = iri.lastIndexOf('#') - if (lastSlashPos < iri.length - 1) { - lastSlashPos + val entityDelimPos = if (lastHashPos >= 0) { + lastHashPos } else { - throw DataConversionException(s"Can't interpret IRI $iri as an entity IRI") + val lastSlashPos = iri.lastIndexOf('/') + + if (lastSlashPos < iri.length - 1) { + lastSlashPos + } else { + throw DataConversionException(s"Can't interpret IRI $iri as an entity IRI") + } } - } - val convertedIriStr = iri.substring(0, entityDelimPos) + val convertedIriStr = iri.substring(0, entityDelimPos) - getOrCacheSmartIri(convertedIriStr, () => new SmartIriImpl(convertedIriStr)) - } + getOrCacheSmartIri(convertedIriStr, () => new SmartIriImpl(convertedIriStr)) + } override def getOntologyFromEntity: SmartIri = ontologyFromEntity @@ -1495,6 +1499,14 @@ class StringFormatter private ( } } + /** + * Constructs a [[SmartIri]] by validating and parsing a string representing an IRI. Throws + * [[DataConversionException]] if the IRI is invalid or is is not an internal representation. + * + * @param iri the IRI string to be parsed. + */ + def toInternalSmartIri(iri: IRI): SmartIri = toSmartIri(iri, requireInternal = true) + /** * Constructs a [[SmartIri]] by validating and parsing a string representing an IRI. * diff --git a/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala b/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala index 5bbb68e8435..cca5b1f27d7 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala @@ -7,6 +7,7 @@ package org.knora.webapi.messages.store.triplestoremessages import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import org.apache.commons.lang3.StringUtils +import play.twirl.api.TxtFormat import spray.json._ import zio._ @@ -254,7 +255,9 @@ case class SparqlUpdateResponse() * * @param sparql the SPARQL string. */ -case class SparqlAskRequest(sparql: String) extends TriplestoreRequest +case class SparqlAskRequest(sparql: String) extends TriplestoreRequest { + def this(txt: TxtFormat.Appendable) = this(txt.toString()) +} /** * Represents a response to a SPARQL ASK query, containing the result. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraResponseV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraResponseV2.scala index 189b9807748..dac5bf466c8 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraResponseV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraResponseV2.scala @@ -10,6 +10,7 @@ import dsp.errors.BadRequestException import org.knora.webapi.ApiV2Complex import org.knora.webapi.ApiV2Schema import org.knora.webapi.ApiV2Simple +import org.knora.webapi.IRI import org.knora.webapi.InternalSchema import org.knora.webapi.OntologySchema import org.knora.webapi.SchemaOption @@ -173,7 +174,8 @@ case class SuccessResponseV2(message: String) extends KnoraJsonLDResponseV2 { * * @param canDo `true` if the operation can be performed. */ -final case class CanDoResponseV2(canDo: Boolean) extends KnoraJsonLDResponseV2 { +final case class CanDoResponseV2(canDo: Boolean, cannotDoReason: Option[String] = None) extends KnoraJsonLDResponseV2 { + require((cannotDoReason.nonEmpty && !canDo) || cannotDoReason.isEmpty) def toJsonLDDocument( targetSchema: ApiV2Schema, appConfig: AppConfig, @@ -182,11 +184,12 @@ final case class CanDoResponseV2(canDo: Boolean) extends KnoraJsonLDResponseV2 { if (targetSchema != ApiV2Complex) { throw BadRequestException(s"Response is available only in the complex schema") } - + val bodyMap: Map[IRI, JsonLDValue] = Map(OntologyConstants.KnoraApiV2Complex.CanDo -> JsonLDBoolean(canDo)) + val reasonMap: Map[IRI, JsonLDValue] = cannotDoReason + .map(reason => Map(OntologyConstants.KnoraApiV2Complex.CannotDoReason -> JsonLDString(reason))) + .getOrElse(Map.empty) JsonLDDocument( - body = JsonLDObject( - Map(OntologyConstants.KnoraApiV2Complex.CanDo -> JsonLDBoolean(canDo)) - ), + body = JsonLDObject(bodyMap ++ reasonMap), context = JsonLDObject( Map( OntologyConstants.KnoraApi.KnoraApiOntologyLabel -> JsonLDString( diff --git a/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala b/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala index e8f3a03311d..4576419a3c8 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala @@ -1,3 +1,8 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + package org.knora.webapi.responders import akka.actor.ActorRef import akka.pattern.ask diff --git a/webapi/src/main/scala/org/knora/webapi/responders/EntityAndClassIriService.scala b/webapi/src/main/scala/org/knora/webapi/responders/EntityAndClassIriService.scala index 970e0fb00d2..cd711708449 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/EntityAndClassIriService.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/EntityAndClassIriService.scala @@ -54,7 +54,7 @@ final case class EntityAndClassIriService( ignoreRdfSubjectAndObject: Boolean = false ): Future[Boolean] = { val query = org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .isEntityUsed(entityIri, ignoreKnoraConstraints, ignoreRdfSubjectAndObject) + .isEntityUsed(entityIri.toInternalIri, ignoreKnoraConstraints, ignoreRdfSubjectAndObject) .toString() appActor .ask(SparqlSelectRequest(query)) 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 eedfc3593fb..705ba2d55a4 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 @@ -99,8 +99,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon changeClassLabelsOrComments(changeClassLabelsOrCommentsRequest) case addCardinalitiesToClassRequest: AddCardinalitiesToClassRequestV2 => addCardinalitiesToClass(addCardinalitiesToClassRequest) - case canChangeCardinalitiesRequest: CanChangeCardinalitiesRequestV2 => - canChangeClassCardinalities(canChangeCardinalitiesRequest) case changeCardinalitiesRequest: ChangeCardinalitiesRequestV2 => changeClassCardinalities(changeCardinalitiesRequest) case canDeleteCardinalitiesFromClassRequestV2: CanDeleteCardinalitiesFromClassRequestV2 => @@ -1476,43 +1474,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon } yield taskResult } - /** - * Checks whether a class's cardinalities can be replaced. - * - * @param canChangeCardinalitiesRequest the request message. - * @return a [[CanDoResponseV2]] indicating whether a class's cardinalities can be replaced. - */ - private def canChangeClassCardinalities( - canChangeCardinalitiesRequest: CanChangeCardinalitiesRequestV2 - ): Future[CanDoResponseV2] = { - val internalClassIri: SmartIri = canChangeCardinalitiesRequest.classIri.toOntologySchema(InternalSchema) - val internalOntologyIri: SmartIri = internalClassIri.getOntologyFromEntity - - for { - cacheData <- Cache.getCacheData - - ontology = cacheData.ontologies.getOrElse( - internalOntologyIri, - throw BadRequestException( - s"Ontology ${canChangeCardinalitiesRequest.classIri.getOntologyFromEntity} does not exist" - ) - ) - - _ = if (!ontology.classes.contains(internalClassIri)) { - throw BadRequestException(s"Class ${canChangeCardinalitiesRequest.classIri} does not exist") - } - - userCanUpdateOntology <- - OntologyHelpers.canUserUpdateOntology(internalOntologyIri, canChangeCardinalitiesRequest.requestingUser) - - classIsUsed <- iriService.isEntityUsed( - entityIri = internalClassIri, - ignoreKnoraConstraints = - true // It's OK if a property refers to the class via knora-base:subjectClassConstraint or knora-base:objectClassConstraint. - ) - } yield CanDoResponseV2(userCanUpdateOntology && !classIsUsed) - } - /** * Replaces a class's cardinalities with new ones. * diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala index c92083a5779..bd440990b15 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/CardinalityHandler.scala @@ -19,7 +19,6 @@ import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi.InternalSchema import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants -import org.knora.webapi.messages.OntologyConstants.KnoraBase import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.SparqlAskRequest @@ -28,6 +27,8 @@ import org.knora.webapi.messages.store.triplestoremessages.SparqlUpdateRequest import org.knora.webapi.messages.store.triplestoremessages.SparqlUpdateResponse import org.knora.webapi.messages.v2.responder.CanDoResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages._ +import org.knora.webapi.queries.sparql.v2 +import org.knora.webapi.slice.resourceinfo.domain.InternalIri /** * Contains methods used for dealing with cardinalities on a class @@ -37,7 +38,7 @@ object CardinalityHandler { /** * FIXME(DSP-1856): Only works if a single cardinality is supplied. * - * @param storeManager the store manager actor. + * @param appActor Reference to the [[org.knora.webapi.core.actors.RoutingActor]] * @param deleteCardinalitiesFromClassRequest the requested cardinalities to be deleted. * @param internalClassIri the Class from which the cardinalities are deleted. * @param internalOntologyIri the Ontology of which the Class and Cardinalities are part of. @@ -122,8 +123,8 @@ object CardinalityHandler { submittedPropertyToDelete: SmartIri = cardinalitiesToDelete.head._1 propertyIsUsed: Boolean <- isPropertyUsedInResources( appActor, - internalClassIri, - submittedPropertyToDelete + internalClassIri.toInternalIri, + submittedPropertyToDelete.toInternalIri ) // Make an update class definition in which the cardinality to delete is removed @@ -157,7 +158,7 @@ object CardinalityHandler { allBaseClassIris: Seq[SmartIri] = internalClassIri +: allBaseClassIrisWithoutInternal - (newInternalClassDefWithLinkValueProps, cardinalitiesForClassWithInheritance) = + (newInternalClassDefWithLinkValueProps, _) = OntologyHelpers .checkCardinalitiesBeforeAddingAndIfNecessaryAddLinkValueProperties( internalClassDef = newClassDefinitionWithRemovedCardinality, @@ -193,7 +194,7 @@ object CardinalityHandler { * Deletes the supplied cardinalities from a class, if the referenced properties are not used in instances * of the class and any subclasses. * - * @param storeManager the store manager actor. + * @param appActor Reference to the [[org.knora.webapi.core.actors.RoutingActor]] * @param deleteCardinalitiesFromClassRequest the requested cardinalities to be deleted. * @param internalClassIri the Class from which the cardinalities are deleted. * @param internalOntologyIri the Ontology of which the Class and Cardinalities are part of. @@ -279,8 +280,8 @@ object CardinalityHandler { submittedPropertyToDelete: SmartIri = cardinalitiesToDelete.head._1 propertyIsUsed: Boolean <- isPropertyUsedInResources( appActor, - internalClassIri, - submittedPropertyToDelete + internalClassIri.toInternalIri, + submittedPropertyToDelete.toInternalIri ) _ = if (propertyIsUsed) { throw BadRequestException("Property is used in data. The cardinality cannot be deleted.") @@ -437,31 +438,21 @@ object CardinalityHandler { * Check if a property entity is used in resource instances. Returns `true` if * it is used, and `false` if it is not used. * - * @param storeManager store manager actor ref. - * @param internalPropertyIri the IRI of the entity that is being checked for usage. + * @param appActor Reference to the [[org.knora.webapi.core.actors.RoutingActor]] + * @param classIri the IRI of the class that is being checked for usage. + * @param propertyIri the IRI of the entity that is being checked for usage. + * * @param ec the execution context onto with the future will run. * @param timeout the timeout for the future. * @return a [[Boolean]] denoting if the property entity is used. */ - def isPropertyUsedInResources( - appActor: ActorRef, - internalClassIri: SmartIri, - internalPropertyIri: SmartIri - )(implicit ec: ExecutionContext, timeout: Timeout): Future[Boolean] = - for { - request <- Future( - org.knora.webapi.queries.sparql.v2.txt - .isPropertyUsed( - internalPropertyIri = internalPropertyIri.toString, - internalClassIri = internalClassIri.toString, - ignoreKnoraConstraints = true, - ignoreRdfSubjectAndObject = true - ) - .toString() - ) - response: SparqlAskResponse <- - appActor.ask(SparqlAskRequest(request)).mapTo[SparqlAskResponse] - } yield response.result + def isPropertyUsedInResources(appActor: ActorRef, classIri: InternalIri, propertyIri: InternalIri)(implicit + ec: ExecutionContext, + timeout: Timeout + ): Future[Boolean] = { + val request = new SparqlAskRequest(v2.txt.isPropertyUsed(propertyIri, classIri)) + appActor.ask(request).mapTo[SparqlAskResponse].map(_.result) + } /** * Checks if the class is defined inside the ontology found in the cache. @@ -505,7 +496,7 @@ object CardinalityHandler { cardinalityInfo: OwlCardinality.KnoraCardinalityInfo, internalClassIri: SmartIri, internalOntologyIri: SmartIri - )(implicit ec: ExecutionContext): Future[Boolean] = { + ): Future[Boolean] = { val currentOntologyState: ReadOntologyV2 = cacheData.ontologies(internalOntologyIri) val readClassInfo: ReadClassInfoV2 = currentOntologyState.classes @@ -539,26 +530,5 @@ object CardinalityHandler { s"Submitted cardinality for property $propertyIri is not defined for class $internalClassIri." ) } - } - - /** - * Checks if the class is a subclass of `knora-base:Resource`. - * - * @param submittedClassInfoContentV2 the class to check - * @return `true` if the class is a subclass of `knora-base:Resource`, otherwise throws an exception. - */ - def isKnoraResourceClass( - submittedClassInfoContentV2: ClassInfoContentV2 - )(implicit ec: ExecutionContext, stringFormatter: StringFormatter): Future[Boolean] = - if (submittedClassInfoContentV2.subClassOf.contains(KnoraBase.Resource.toSmartIri)) { - FastFuture.successful(true) - } else { - FastFuture.failed( - throw BadRequestException( - s"Class ${submittedClassInfoContentV2.classIri} is not a subclass of ${KnoraBase.Resource.toSmartIri}. $submittedClassInfoContentV2" - ) - ) - } - } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala index 2c466648d7a..ef67321bdde 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -21,6 +21,7 @@ import org.knora.webapi.http.version.ServerVersion import org.knora.webapi.routing.admin._ import org.knora.webapi.routing.v1._ import org.knora.webapi.routing.v2._ +import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService trait ApiRoutes { @@ -32,8 +33,11 @@ object ApiRoutes { /** * All routes composed together. */ - val layer - : ZLayer[State with RestResourceInfoService with AppConfig with AppRouter with ActorSystem, Nothing, ApiRoutes] = + val layer: ZLayer[ + AppConfig with AppRouter with RestCardinalityService with RestResourceInfoService with State with ActorSystem, + Nothing, + ApiRoutes + ] = ZLayer { for { sys <- ZIO.service[ActorSystem] @@ -46,7 +50,7 @@ object ApiRoutes { appConfig = appConfig ) ) - runtime <- ZIO.runtime[core.State with RestResourceInfoService] + runtime <- ZIO.runtime[core.State with RestResourceInfoService with RestCardinalityService] } yield ApiRoutesImpl(routeData, runtime, appConfig) } } @@ -60,7 +64,7 @@ object ApiRoutes { */ private final case class ApiRoutesImpl( routeData: KnoraRouteData, - runtime: Runtime[core.State with RestResourceInfoService], + runtime: Runtime[core.State with RestResourceInfoService with RestCardinalityService], appConfig: AppConfig ) extends ApiRoutes with AroundDirectives { @@ -85,7 +89,7 @@ private final case class ApiRoutesImpl( new CkanRouteV1(routeData).makeRoute ~ new UsersRouteV1(routeData).makeRoute ~ new ProjectsRouteV1(routeData).makeRoute ~ - new OntologiesRouteV2(routeData).makeRoute ~ + new OntologiesRouteV2(routeData, runtime).makeRoute ~ new SearchRouteV2(routeData).makeRoute ~ new ResourcesRouteV2(routeData, runtime).makeRoute ~ new ValuesRouteV2(routeData).makeRoute ~ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala index 9537a0ed9a6..4178b50dba7 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala @@ -23,6 +23,8 @@ import pdi.jwt.JwtClaim import pdi.jwt.JwtHeader import pdi.jwt.JwtSprayJson import spray.json._ +import zio.Task +import zio.ZIO import java.util.Base64 import java.util.UUID @@ -413,6 +415,12 @@ trait Authenticator extends InstrumentationSupport { // GET USER PROFILE / AUTHENTICATION ENTRY POINT //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + def getUserADMZio(requestContext: RequestContext, appConfig: AppConfig)(implicit + system: ActorSystem, + appActor: ActorRef, + executionContext: ExecutionContext + ): Task[UserADM] = ZIO.fromFuture(_ => getUserADM(requestContext, appConfig)) + /** * Returns a User that match the credentials found in the [[RequestContext]]. * The credentials can be email/password as parameters or auth headers, or session token in a cookie header. If no diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala index e45c935b12e..c86e231f348 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala @@ -6,12 +6,14 @@ package org.knora.webapi.routing import akka.actor.ActorRef +import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model._ import akka.http.scaladsl.server.RequestContext import akka.http.scaladsl.server.RouteResult import akka.pattern._ import akka.util.Timeout import com.typesafe.scalalogging.Logger +import zio.ZIO import scala.concurrent.ExecutionContext import scala.concurrent.Future @@ -102,6 +104,9 @@ object RouteUtilV2 { */ val JSON_LD_RENDERING_HIERARCHICAL: String = "hierarchical" + def getStringQueryParam(ctx: RequestContext, key: String): Option[String] = getQueryParamsMap(ctx).get(key) + private def getQueryParamsMap(ctx: RequestContext): Map[String, String] = ctx.request.uri.query().toMap + /** * Gets the ontology schema that is specified in an HTTP request. The schema can be specified * either in the HTTP header [[SCHEMA_HEADER]] or in the URL parameter [[SCHEMA_PARAM]]. @@ -233,43 +238,43 @@ object RouteUtilV2 { targetSchema: OntologySchema, schemaOptions: Set[SchemaOption] )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[RouteResult] = { + val askResponse = (appActor.ask(requestMessage)).map { + case replyMessage: KnoraResponseV2 => replyMessage + + case other => + // The responder returned an unexpected message type (not an exception). This isn't the client's + // fault, so log it and return an error message to the client. + throw UnexpectedMessageException( + s"Responder sent a reply of type ${other.getClass.getCanonicalName}" + ) + } + completeResponse(askResponse, requestContext, appConfig, targetSchema, schemaOptions) + } - val httpResponse: Future[HttpResponse] = for { - // Make sure the responder sent a reply of type KnoraResponseV2. - knoraResponse <- (appActor.ask(requestMessage)).map { - case replyMessage: KnoraResponseV2 => replyMessage - - case other => - // The responder returned an unexpected message type (not an exception). This isn't the client's - // fault, so log it and return an error message to the client. - throw UnexpectedMessageException( - s"Responder sent a reply of type ${other.getClass.getCanonicalName}" - ) - } - - // Choose a media type for the response. - responseMediaType: MediaType.NonBinary = chooseRdfMediaTypeForResponse(requestContext) - - // Find the most specific media type that is compatible with the one requested. - specificMediaType: MediaType.NonBinary = RdfMediaTypes.toMostSpecificMediaType(responseMediaType) - - // Convert the requested media type to a UTF-8 content type. - contentType: ContentType.NonBinary = RdfMediaTypes.toUTF8ContentType(responseMediaType) - - // Format the response message. - formattedResponseContent: String = knoraResponse.format( - rdfFormat = RdfFormat.fromMediaType(specificMediaType), - targetSchema = targetSchema, - appConfig = appConfig, - schemaOptions = schemaOptions - ) - } yield HttpResponse( - status = StatusCodes.OK, - entity = HttpEntity( - contentType, - formattedResponseContent - ) - ) + def completeZioApiV2ComplexResponse[R]( + responseZio: ZIO[R, Throwable, KnoraResponseV2], + ctx: RequestContext, + config: AppConfig + )(implicit ec: ExecutionContext, runtime: zio.Runtime[R]): Future[RouteResult] = { + val responseFuture = UnsafeZioRun.runToFuture(responseZio) + completeResponse(responseFuture, ctx, config, ApiV2Complex, RouteUtilV2.getSchemaOptions(ctx)) + } + + private def completeResponse( + responseFuture: Future[KnoraResponseV2], + requestContext: RequestContext, + appConfig: AppConfig, + targetSchema: OntologySchema, + schemaOptions: Set[SchemaOption] + )(implicit ec: ExecutionContext): Future[RouteResult] = { + + val httpResponse = for { + knoraResponse <- responseFuture + responseMediaType = chooseRdfMediaTypeForResponse(requestContext) + rdfFormat = RdfFormat.fromMediaType(RdfMediaTypes.toMostSpecificMediaType(responseMediaType)) + contentType = RdfMediaTypes.toUTF8ContentType(responseMediaType) + content: String = knoraResponse.format(rdfFormat, targetSchema, schemaOptions, appConfig) + } yield HttpResponse(StatusCodes.OK, entity = HttpEntity(contentType, content)) requestContext.complete(httpResponse) } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala index 415cea03cb6..e98c10c438f 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala @@ -1,3 +1,8 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + package org.knora.webapi.routing import zio.Task import zio.ZIO diff --git a/webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala b/webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala new file mode 100644 index 00000000000..9620e508ae8 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/UnsafeZioRun.scala @@ -0,0 +1,19 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.routing + +import zio._ + +import scala.concurrent.Future + +object UnsafeZioRun { + + def run[R, E, A](zioAction: ZIO[R, E, A])(implicit r: Runtime[R]): Exit[E, A] = + Unsafe.unsafe(implicit u => r.unsafe.run(zioAction)) + + def runToFuture[R, A](zioAction: ZIO[R, Throwable, A])(implicit r: Runtime[R]): Future[A] = + Unsafe.unsafe(implicit u => r.unsafe.runToFuture(zioAction)) +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala index 398a8eaac71..27a75712694 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala @@ -1,3 +1,8 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + package org.knora.webapi.routing.admin import akka.actor.ActorRef diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala index e2108026e73..f706988322f 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/OntologiesRouteV2.scala @@ -36,11 +36,16 @@ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilV2 +import org.knora.webapi.routing.RouteUtilV2.completeZioApiV2ComplexResponse +import org.knora.webapi.routing.RouteUtilV2.getStringQueryParam +import org.knora.webapi.slice.ontology.api.service.RestCardinalityService /** * Provides a routing function for API v2 routes that deal with ontologies. */ -class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class OntologiesRouteV2(routeData: KnoraRouteData, implicit val runtime: zio.Runtime[RestCardinalityService]) + extends KnoraRoute(routeData) + with Authenticator { val ontologiesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "ontologies") @@ -60,7 +65,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) updateClass() ~ deleteClassComment() ~ addCardinalities() ~ - canReplaceCardinalities() ~ + canReplaceCardinalities ~ replaceCardinalities() ~ canDeleteCardinalitiesFromClass() ~ deleteCardinalitiesFromClass() ~ @@ -430,35 +435,22 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def canReplaceCardinalities(): Route = - path(ontologiesBasePath / "canreplacecardinalities" / Segment) { classIriStr: IRI => + private def canReplaceCardinalities: Route = + // GET basePath/{iriEncode} or + // GET basePath/{iriEncode}?propertyIri={iriEncode}&newCardinality=[0-1|1|1-n|0-n] + path(ontologiesBasePath / "canreplacecardinalities" / Segment) { classIri: IRI => get { requestContext => - val classIri = classIriStr.toSmartIri - stringFormatter.checkExternalOntologyName(classIri) - - if (!classIri.getOntologySchema.contains(ApiV2Complex)) { - throw BadRequestException(s"Invalid class IRI for request: $classIriStr") - } - - val requestMessageFuture: Future[CanChangeCardinalitiesRequestV2] = for { - requestingUser <- getUserADM( - requestContext = requestContext, - routeData.appConfig - ) - } yield CanChangeCardinalitiesRequestV2( - classIri = classIri, - requestingUser = requestingUser - ) - - RouteUtilV2.runRdfRouteWithFuture( - requestMessageF = requestMessageFuture, - requestContext = requestContext, - appConfig = routeData.appConfig, - appActor = appActor, - log = log, - targetSchema = ApiV2Complex, - schemaOptions = RouteUtilV2.getSchemaOptions(requestContext) - ) + val appConfig = routeData.appConfig + val responseZio = getUserADMZio(requestContext, appConfig) + .flatMap(user => + RestCardinalityService.canUpdateCardinality( + classIri, + user, + propertyIri = getStringQueryParam(requestContext, RestCardinalityService.propertyIriKey), + newCardinality = getStringQueryParam(requestContext, RestCardinalityService.newCardinalityKey) + ) + ) + completeZioApiV2ComplexResponse(responseZio, requestContext, appConfig) } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/service/Repository.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/service/Repository.scala new file mode 100644 index 00000000000..94fc4c25662 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/service/Repository.scala @@ -0,0 +1,112 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.common.service + +import zio._ + +/** + * Trait for generic readonly operations on a repository for a specific type. + * + * @tparam Entity the type of the entity. + * @tparam Id the type of the id of the entities. + */ +trait Repository[Entity, Id] { + + /** + * Retrieves an entity by its id. + * + * @param id The identifier of type [[Id]]. + * @return the entity with the given id or [[None]] if none found. + */ + def findById(id: Id): Task[Option[Entity]] + + /** + * Returns all instances of the type [[Entity]] with the given IDs. + * If some or all ids are not found, no entities are returned for these IDs. + * Note that the order of elements in the result is not guaranteed. + * + * @param ids A sequence of identifiers of type [[Id]]. + * @return All found entities. The size can be equal or less than the number of given ids. + */ + def findAllById(ids: Seq[Id]): Task[List[Entity]] = ZIO.foreach(ids)(findById).map(_.flatten.toList) + + /** + * Returns whether an entity with the given id exists. + * + * @param id The identifier of type [[Id]]. + * @return true if an entity with the given id exists, false otherwise. + */ + def existsById(id: Id): Task[Boolean] = findById(id).map(_.isDefined) + + /** + * Returns the number of entities available. + * + * @return the number of entities. + */ + def count(): Task[Long] = findAll().map(_.size) + + /** + * Returns all instances of the type. + * + * @return all instances of the type. + */ + def findAll(): Task[List[Entity]] +} + +/** + * Trait for generic CRUD (create, read, update, delete) operations on a repository for a specific type. + * + * @tparam Entity the type of the entity. + * @tparam Id the type of the id of the entities. + */ +trait CrudRepository[Entity, Id] extends Repository[Entity, Id] { + + /** + * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the entity instance completely. + * + * @param entity The entity to be saved. + * @return the saved entity. + */ + def save(entity: Entity): Task[Entity] + + /** + * Saves all given entities. + * + * @param entities The entities to be saved + * @return the saved entities. The returned [[List]] will have the same size as the entities passed as an argument. + */ + def saveAll(entities: Seq[Entity]): Task[List[Entity]] = ZIO.foreach(entities)(save).map(_.toList) + + /** + * Deletes a given entity. + * + * @param entity The entity to be deleted + */ + def delete(entity: Entity): Task[Unit] + + /** + * Deletes all given entities. + * + * @param entities The entities to be deleted + */ + def deleteAll(entities: Seq[Entity]): Task[Unit] = ZIO.foreach(entities)(delete).unit + + /** + * Deletes the entity with the given id. + * If the entity is not found in the persistence store it is silently ignored. + * + * @param id The identifier to the entity to be deleted + */ + def deleteById(id: Id): Task[Unit] + + /** + * Deletes all instances of the type [[Entity]] with the given IDs. + * Entities that aren't found in the persistence store are silently ignored. + * + * @param ids The identifiers to the entities to be deleted + */ + def deleteAllById(ids: Seq[Id]): Task[Unit] = ZIO.foreach(ids)(deleteById).unit +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/RestCardinalityService.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/RestCardinalityService.scala new file mode 100644 index 00000000000..9175ffa34b0 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/RestCardinalityService.scala @@ -0,0 +1,122 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.api.service +import zio.IO +import zio.Task +import zio.ZIO +import zio.ZLayer +import zio.macros.accessible + +import dsp.errors.BadRequestException.invalidQueryParamValue +import dsp.errors.BadRequestException.missingQueryParamValue +import dsp.errors.ForbiddenException +import org.knora.webapi.IRI +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.messages.v2.responder.CanDoResponseV2 +import org.knora.webapi.slice.ontology.api.service.RestCardinalityService.classIriKey +import org.knora.webapi.slice.ontology.api.service.RestCardinalityService.newCardinalityKey +import org.knora.webapi.slice.ontology.api.service.RestCardinalityService.propertyIriKey +import org.knora.webapi.slice.ontology.domain.model.Cardinality +import org.knora.webapi.slice.ontology.domain.service.CardinalityService +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.ChangeCardinalityCheckResult +import org.knora.webapi.slice.ontology.domain.service.OntologyRepo +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +@accessible +trait RestCardinalityService { + + def canUpdateCardinality( + classIri: IRI, + user: UserADM, + propertyIri: Option[IRI], + newCardinality: Option[String] + ): Task[CanDoResponseV2] = + (propertyIri, newCardinality) match { + case (None, Some(_)) => ZIO.fail(missingQueryParamValue(propertyIriKey)) + case (Some(_), None) => ZIO.fail(missingQueryParamValue(newCardinalityKey)) + case (None, None) => canReplaceCardinality(classIri, user) + case (Some(propertyIri), Some(newCardinality)) => canSetCardinality(classIri, propertyIri, newCardinality, user) + } + + def canReplaceCardinality(classIri: IRI, user: UserADM): Task[CanDoResponseV2] + + def canSetCardinality( + classIri: IRI, + propertyIri: IRI, + cardinality: String, + user: UserADM + ): Task[CanDoResponseV2] +} + +private final case class PermissionService(ontologyRepo: OntologyRepo) { + def hasOntologyWriteAccess(user: UserADM, ontologyIri: InternalIri): Task[Boolean] = { + val permissions = user.permissions + for { + data <- ontologyRepo.findById(ontologyIri) + projectIriMaybe = data.flatMap(_.ontologyMetadata.projectIri.map(_.toIri)) + hasPermission = projectIriMaybe.exists(permissions.isSystemAdmin || permissions.isProjectAdmin(_)) + } yield hasPermission + } +} + +case class RestCardinalityServiceLive( + cardinalityService: CardinalityService, + iriConverter: IriConverter, + ontologyRepo: OntologyRepo +) extends RestCardinalityService { + + private val permissionService: PermissionService = PermissionService(ontologyRepo) + + def canReplaceCardinality(classIri: IRI, user: UserADM): Task[CanDoResponseV2] = + for { + classIri <- iriConverter.asInternalIri(classIri).orElseFail(invalidQueryParamValue(classIriKey)) + _ <- checkUserHasWriteAccessToOntologyOfClass(user, classIri) + result <- cardinalityService.canReplaceCardinality(classIri) + } yield toResponse(result) + + private def checkUserHasWriteAccessToOntologyOfClass(user: UserADM, classIri: InternalIri): Task[Unit] = { + val hasWriteAccess = for { + ontologyIri <- iriConverter.getOntologyIriFromClassIri(classIri) + hasWriteAccess <- permissionService.hasOntologyWriteAccess(user, ontologyIri) + } yield hasWriteAccess + ZIO.ifZIO(hasWriteAccess)( + onTrue = ZIO.unit, + onFalse = ZIO.fail(ForbiddenException(s"User has no write access to ontology")) + ) + } + + def canSetCardinality( + classIri: IRI, + propertyIri: IRI, + cardinality: String, + user: UserADM + ): Task[CanDoResponseV2] = + for { + classIri <- iriConverter.asInternalIri(classIri).orElseFail(invalidQueryParamValue(classIriKey)) + _ <- checkUserHasWriteAccessToOntologyOfClass(user, classIri) + newCardinality <- parseCardinality(cardinality).orElseFail(invalidQueryParamValue(newCardinalityKey)) + propertyIri <- iriConverter.asInternalIri(propertyIri).orElseFail(invalidQueryParamValue(propertyIriKey)) + result <- cardinalityService.canSetCardinality(classIri, propertyIri, newCardinality) + } yield toResponse(result) + + private def parseCardinality(cardinality: String): IO[Option[Nothing], Cardinality] = + ZIO.fromOption(Cardinality.fromString(cardinality)) + + private def toResponse(result: ChangeCardinalityCheckResult): CanDoResponseV2 = result match { + case _: ChangeCardinalityCheckResult.Success => CanDoResponseV2(canDo = true) + case failure: ChangeCardinalityCheckResult.Failure => CanDoResponseV2(canDo = false, Some(failure.reason)) + } +} + +object RestCardinalityService { + val classIriKey: String = "classIri" + val propertyIriKey: String = "propertyIri" + val newCardinalityKey: String = "newCardinality" + val layer: ZLayer[CardinalityService with IriConverter with OntologyRepo, Nothing, RestCardinalityService] = + ZLayer.fromFunction(RestCardinalityServiceLive.apply _) +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/model/Cardinality.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/model/Cardinality.scala new file mode 100644 index 00000000000..fb1057b9ac4 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/model/Cardinality.scala @@ -0,0 +1,65 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.domain.model +import dsp.schema.domain.Cardinality.MayHaveMany +import dsp.schema.domain.Cardinality.MayHaveOne +import dsp.schema.domain.Cardinality.MustHaveOne +import dsp.schema.domain.Cardinality.MustHaveSome +import dsp.schema.domain.{Cardinality => OldCardinality} +import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.KnoraCardinalityInfo + +sealed trait Cardinality { + def min: Int + def max: Option[Int] + + val oldCardinality: OldCardinality + + def isStricterThan(other: Cardinality): Boolean = + (other.min, this.min, other.max, this.max) match { + case (otherMin, thisMin, _, _) if otherMin < thisMin => true + case (_, _, otherMax, Some(thisMax)) => otherMax.forall(_ > thisMax) + case _ => false + } + + override def toString: String = (min, max) match { + case (min, None) => s"$min-n" + case (min, Some(max)) => if (min == max) s"$min" else s"$min-$max" + } +} + +object Cardinality { + case object AtLeastOne extends Cardinality { + override val min: Int = 1 + override val max: Option[Int] = None + override val oldCardinality: OldCardinality = MustHaveSome + } + + case object ExactlyOne extends Cardinality { + override val min: Int = 1 + override val max: Option[Int] = Some(1) + override val oldCardinality: OldCardinality = MustHaveOne + } + + case object Unbounded extends Cardinality { + override val min: Int = 0 + override val max: Option[Int] = None + override val oldCardinality: OldCardinality = MayHaveMany + } + + case object ZeroOrOne extends Cardinality { + override val min: Int = 0 + override val max: Option[Int] = Some(1) + override val oldCardinality: OldCardinality = MayHaveOne + } + + val allCardinalities: Array[Cardinality] = Array(AtLeastOne, ExactlyOne, Unbounded, ZeroOrOne) + + def get(cardinalityInfo: KnoraCardinalityInfo): Cardinality = get(cardinalityInfo.cardinality) + def get(cardinality: OldCardinality): Cardinality = + allCardinalities.find(_.oldCardinality == cardinality).getOrElse(throw new IllegalStateException) + + def fromString(str: String): Option[Cardinality] = allCardinalities.find(_.toString == str) +} 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 new file mode 100644 index 00000000000..f1b994cabdf --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/CardinalityService.scala @@ -0,0 +1,283 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.domain.service +import akka.actor.ActorRef +import akka.util.Timeout +import zio.Task +import zio.ZIO +import zio.ZLayer +import zio.macros.accessible + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.twirl.queries +import org.knora.webapi.messages.v2.responder.CanDoResponseV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.CanDeleteCardinalitiesFromClassRequestV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.DeleteCardinalitiesFromClassRequestV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 +import org.knora.webapi.queries.sparql._ +import org.knora.webapi.responders.ActorDeps +import org.knora.webapi.responders.v2.ontology.CardinalityHandler +import org.knora.webapi.slice.ontology.domain.model.Cardinality +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanReplaceCardinalityCheckResult +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanReplaceCardinalityCheckResult.CanReplaceCardinalityCheckResult +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanReplaceCardinalityCheckResult.CanReplaceCheckSuccess +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanReplaceCardinalityCheckResult.IsInUseCheckFailure +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanSetCardinalityCheckResult +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanSetCardinalityCheckResult.CanSetCardinalityCheckResult +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.store.triplestore.api.TriplestoreService + +@accessible +trait CardinalityService { + + /** + * Check if a specific cardinality may be set on a property in the context of a class. + * + * Setting a wider cardinality on a sub class is not possible if for the same property a stricter cardinality already exists in one of its super classes. + * + * @param classIri class to check + * @param propertyIri property to check + * @param newCardinality the newly desired cardinality + * @return + * '''success''' a [[CanSetCardinalityCheckResult]] indicating whether a class's cardinalities can be set. + * + * '''error''' a [[Throwable]] indicating that something went wrong, + */ + def canSetCardinality( + classIri: InternalIri, + propertyIri: InternalIri, + newCardinality: Cardinality + ): Task[CanSetCardinalityCheckResult] + + /** + * Check if a specific cardinality may be replaced. + * + * Replacing an existing cardinality is only possible it is not in use yet. + * + * @param classIri class to check + * @return + * '''success''' a [[CanReplaceCardinalityCheckResult]] indicating whether a class's cardinalities can be replaced. + * + * '''error''' a [[Throwable]] indicating that something went wrong. + */ + def canReplaceCardinality(classIri: InternalIri): Task[CanReplaceCardinalityCheckResult] + + /** + * FIXME(DSP-1856): Only works if a single cardinality is supplied. + * + * @param deleteCardinalitiesFromClassRequest the requested cardinalities to be deleted. + * @param internalClassIri the Class from which the cardinalities are deleted. + * @param internalOntologyIri the Ontology of which the Class and Cardinalities are part of. + * @return a [[CanDoResponseV2]] indicating whether a class's cardinalities can be deleted. + */ + def canDeleteCardinalitiesFromClass( + deleteCardinalitiesFromClassRequest: CanDeleteCardinalitiesFromClassRequestV2, + internalClassIri: SmartIri, + internalOntologyIri: SmartIri + ): Task[CanDoResponseV2] + + /** + * Deletes the supplied cardinalities from a class, if the referenced properties are not used in instances + * of the class and any subclasses. + * + * FIXME(DSP-1856): Only works if a single cardinality is supplied. + * + * @param deleteCardinalitiesFromClassRequest the requested cardinalities to be deleted. + * @param internalClassIri the Class from which the cardinalities are deleted. + * @param internalOntologyIri the Ontology of which the Class and Cardinalities are part of. + * @return a [[ReadOntologyV2]] in the internal schema, containing the new class definition. + */ + def deleteCardinalitiesFromClass( + deleteCardinalitiesFromClassRequest: DeleteCardinalitiesFromClassRequestV2, + internalClassIri: SmartIri, + internalOntologyIri: SmartIri + ): Task[ReadOntologyV2] + + /** + * Check if a property entity is used in resource instances. Returns `true` if + * it is used, and `false` if it is not used. + * + * @param classIri the IRI of the class that is being checked for usage. + * @param propertyIri the IRI of the entity that is being checked for usage. + * + * @return a [[Boolean]] denoting if the property entity is used. + */ + def isPropertyUsedInResources(classIri: InternalIri, propertyIri: InternalIri): Task[Boolean] +} + +object ChangeCardinalityCheckResult { + + sealed trait ChangeCardinalityCheckResult { + def isSuccess: Boolean + } + + trait Success extends ChangeCardinalityCheckResult { + override final val isSuccess: Boolean = true + } + trait Failure extends ChangeCardinalityCheckResult { + override final val isSuccess: Boolean = false + val reason: String + } + + abstract class KnoraOntologyCheckFailure extends Failure { + override final val reason: String = "A base class exists which is more restrictive." + } + + object CanReplaceCardinalityCheckResult { + sealed trait CanReplaceCardinalityCheckResult extends ChangeCardinalityCheckResult + final case object CanReplaceCheckSuccess extends Success with CanReplaceCardinalityCheckResult + final case object IsInUseCheckFailure extends Failure with CanReplaceCardinalityCheckResult { + override val reason: String = "Cardinality is in use" + } + final case object KnoraOntologyCheckFailure + extends ChangeCardinalityCheckResult.KnoraOntologyCheckFailure + with CanReplaceCardinalityCheckResult + } + + object CanSetCardinalityCheckResult { + sealed trait CanSetCardinalityCheckResult extends ChangeCardinalityCheckResult + final case object CanSetCheckSuccess extends Success with CanSetCardinalityCheckResult + final case object BaseClassCheckFailure extends Failure with CanSetCardinalityCheckResult { + val reason: String = "A base class exists which is more restrictive." + } + final case object KnoraOntologyCheckFailure + extends ChangeCardinalityCheckResult.KnoraOntologyCheckFailure + with CanSetCardinalityCheckResult + } +} +final case class CardinalityServiceLive( + private val actorDeps: ActorDeps, + private val stringFormatter: StringFormatter, + private val tripleStore: TriplestoreService, + private val ontologyRepo: OntologyRepo, + private val iriConverter: IriConverter +) extends CardinalityService { + private implicit val ec: ExecutionContext = actorDeps.executionContext + private implicit val timeout: Timeout = actorDeps.timeout + private implicit val sf: StringFormatter = stringFormatter + + private val appActor: ActorRef = actorDeps.appActor + + private def toTask[A]: Future[A] => Task[A] = f => ZIO.fromFuture(_ => f) + + override def canDeleteCardinalitiesFromClass( + deleteCardinalitiesFromClassRequest: CanDeleteCardinalitiesFromClassRequestV2, + internalClassIri: SmartIri, + internalOntologyIri: SmartIri + ): Task[CanDoResponseV2] = + toTask( + CardinalityHandler.canDeleteCardinalitiesFromClass( + appActor, + deleteCardinalitiesFromClassRequest, + internalClassIri, + internalOntologyIri + ) + ) + + /** + * FIXME(DSP-1856): Only works if a single cardinality is supplied. + * Deletes the supplied cardinalities from a class, if the referenced properties are not used in instances + * of the class and any subclasses. + * + * @param deleteCardinalitiesFromClassRequest the requested cardinalities to be deleted. + * @param internalClassIri the Class from which the cardinalities are deleted. + * @param internalOntologyIri the Ontology of which the Class and Cardinalities are part of. + * @return a [[ReadOntologyV2]] in the internal schema, containing the new class definition. + */ + override def deleteCardinalitiesFromClass( + deleteCardinalitiesFromClassRequest: DeleteCardinalitiesFromClassRequestV2, + internalClassIri: SmartIri, + internalOntologyIri: SmartIri + ): Task[ReadOntologyV2] = toTask( + CardinalityHandler.deleteCardinalitiesFromClass( + appActor, + deleteCardinalitiesFromClassRequest, + internalClassIri, + internalOntologyIri + ) + ) + + def isPropertyUsedInResources(classIri: InternalIri, propertyIri: InternalIri): Task[Boolean] = { + val query = v2.txt.isPropertyUsed(propertyIri, classIri) + tripleStore.sparqlHttpAsk(query.toString).map(_.result) + } + + override def canSetCardinality( + classIri: InternalIri, + propertyIri: InternalIri, + newCardinality: Cardinality + ): Task[CanSetCardinalityCheckResult] = + ZIO.ifZIO(isPartOfKnoraOntology(classIri))( + onTrue = ZIO.succeed(CanSetCardinalityCheckResult.KnoraOntologyCheckFailure), + onFalse = doesSuperClassExistWithStricterCardinality(classIri, propertyIri, newCardinality).map { + case false => CanSetCardinalityCheckResult.CanSetCheckSuccess + case true => CanSetCardinalityCheckResult.BaseClassCheckFailure + } + ) + + private val knoraAdminAndBaseOntologies = Seq( + "http://www.knora.org/ontology/knora-base", + "http://www.knora.org/ontology/knora-admin" + ).map(InternalIri) + + private def isPartOfKnoraOntology(classIri: InternalIri): Task[Boolean] = + iriConverter.getOntologyIriFromClassIri(classIri).map(knoraAdminAndBaseOntologies.contains) + + private def doesSuperClassExistWithStricterCardinality( + classIri: InternalIri, + propertyIri: InternalIri, + newCardinality: Cardinality + ) = + for { + propSmartIri <- iriConverter.asInternalSmartIri(propertyIri) + classInfoMaybe <- ontologyRepo.findClassBy(classIri) + inheritedCardinalities = classInfoMaybe.flatMap(_.inheritedCardinalities.get(propSmartIri)).map(Cardinality.get) + } yield inheritedCardinalities.forall(_.isStricterThan(newCardinality)) + + /** + * Check if a specific cardinality may be replaced. + * + * Replacing an existing cardinality on a class is only possible if it is not in use. + * + * @param classIri class to check + * @return + * '''success''' a [[CanSetCardinalityCheckResult]] indicating whether a class's cardinalities can be set. + * + * '''error''' a [[Throwable]] indicating that something went wrong, + */ + override def canReplaceCardinality(classIri: InternalIri): Task[CanReplaceCardinalityCheckResult] = { + val doCheck: Task[CanReplaceCardinalityCheckResult] = { + // ignoreKnoraConstraints: It is OK if a property refers to the class + // via knora-base:subjectClassConstraint or knora-base:objectClassConstraint. + val query = queries.sparql.v2.txt.isEntityUsed(classIri, ignoreKnoraConstraints = true) + tripleStore + .sparqlHttpSelect(query.toString()) + .map(_.results.bindings) + .map { + case seq if seq.isEmpty => CanReplaceCheckSuccess + case _ => IsInUseCheckFailure + } + } + + ZIO.ifZIO(isPartOfKnoraOntology(classIri))( + onTrue = ZIO.succeed(CanReplaceCardinalityCheckResult.KnoraOntologyCheckFailure), + onFalse = doCheck + ) + } +} + +object CardinalityService { + val layer: ZLayer[ + ActorDeps with StringFormatter with TriplestoreService with OntologyRepo with IriConverter, + Nothing, + CardinalityServiceLive + ] = ZLayer.fromFunction(CardinalityServiceLive.apply _) +} 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 new file mode 100644 index 00000000000..c3b7fd70fad --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala @@ -0,0 +1,21 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.domain.service +import zio.Task +import zio.macros.accessible + +import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 +import org.knora.webapi.slice.common.service.Repository +import org.knora.webapi.slice.resourceinfo.domain.InternalIri + +@accessible +trait OntologyRepo extends Repository[ReadOntologyV2, InternalIri] { + + override def findById(id: InternalIri): Task[Option[ReadOntologyV2]] + override def findAll(): Task[List[ReadOntologyV2]] + def findClassBy(iri: InternalIri): Task[Option[ReadClassInfoV2]] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCache.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCache.scala new file mode 100644 index 00000000000..a35d6f8f55b --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCache.scala @@ -0,0 +1,26 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.repo.service +import zio.Task +import zio.ULayer +import zio.ZIO +import zio.ZLayer +import zio.macros.accessible + +import org.knora.webapi.responders.v2.ontology.Cache + +@accessible +trait OntologyCache { + def get: Task[Cache.OntologyCacheData] +} + +final case class OntologyCacheLive() extends OntologyCache { + def get: Task[Cache.OntologyCacheData] = ZIO.fromFuture(implicit ec => Cache.getCacheData) +} + +object OntologyCache { + val layer: ULayer[OntologyCacheLive] = ZLayer.succeed(OntologyCacheLive()) +} 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 new file mode 100644 index 00000000000..9701d5f0386 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala @@ -0,0 +1,41 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.repo.service + +import zio.Task +import zio.ZLayer + +import org.knora.webapi.messages.SmartIri +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.service.OntologyRepo +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +final case class OntologyRepoLive(private val converter: IriConverter, private val ontologyCache: OntologyCache) + extends OntologyRepo { + + override def findById(iri: InternalIri): Task[Option[ReadOntologyV2]] = + converter.asInternalSmartIri(iri).flatMap(findBySmartIri) + + private def findBySmartIri(ontologyIri: SmartIri): Task[Option[ReadOntologyV2]] = + getOntologiesMap.map(_.get(ontologyIri)) + + private def getOntologiesMap: Task[Map[SmartIri, ReadOntologyV2]] = ontologyCache.get.map(_.ontologies) + + override def findClassBy(iri: InternalIri): Task[Option[ReadClassInfoV2]] = for { + ontologyIri <- converter.getOntologyIriFromClassIri(iri) + ontology <- findById(ontologyIri) + classIri <- converter.asInternalSmartIri(iri) + } yield ontology.flatMap(_.classes.get(classIri)) + + override def findAll(): Task[List[ReadOntologyV2]] = getOntologiesMap.map(_.values.toList) +} + +object OntologyRepoLive { + val layer: ZLayer[IriConverter with OntologyCache, Nothing, OntologyRepoLive] = + ZLayer.fromFunction(OntologyRepoLive.apply _) +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/InternalIri.scala b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/InternalIri.scala index 3fe2159adfb..68a935c46c9 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/InternalIri.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/InternalIri.scala @@ -7,4 +7,4 @@ package org.knora.webapi.slice.resourceinfo.domain import org.knora.webapi.IRI -final case class InternalIri(value: IRI) +case class InternalIri(value: IRI) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/IriConverter.scala b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/IriConverter.scala index 80255f251ac..90ff6977a36 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/IriConverter.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/domain/IriConverter.scala @@ -11,18 +11,25 @@ import zio.ZLayer import zio.macros.accessible import org.knora.webapi.IRI +import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter @accessible trait IriConverter { - def asInternalIri(iri: IRI): Task[InternalIri] + def asInternalSmartIri(iri: IRI): Task[SmartIri] + def asSmartIri(iri: IRI): Task[SmartIri] + + def asInternalIri(iri: IRI): Task[InternalIri] = asSmartIri(iri).mapAttempt(_.toInternalIri) + def asInternalSmartIri(iri: InternalIri): Task[SmartIri] = asInternalSmartIri(iri.value) + def getOntologyIriFromClassIri(iri: InternalIri): Task[InternalIri] = + getOntologySmartIriFromClassIri(iri).mapAttempt(_.toInternalIri) + def getOntologySmartIriFromClassIri(iri: InternalIri): Task[SmartIri] = + asInternalSmartIri(iri.value).mapAttempt(_.getOntologyFromEntity) } -final case class IriConverterLive(stringFormatter: StringFormatter) extends IriConverter { - def asInternalIri(iri: IRI): Task[InternalIri] = - ZIO.attempt { - stringFormatter.toSmartIri(iri).internalIri - }.map(InternalIri) +final case class IriConverterLive(sf: StringFormatter) extends IriConverter { + override def asInternalSmartIri(iri: IRI): Task[SmartIri] = ZIO.attempt(sf.toInternalSmartIri(iri)) + override def asSmartIri(iri: IRI): Task[SmartIri] = ZIO.attempt(sf.toSmartIri(iri)) } object IriConverter { diff --git a/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala b/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala index a9884c43939..5f308375920 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ActorUtil.scala @@ -23,7 +23,6 @@ import dsp.errors.ExceptionUtil import dsp.errors.NotFoundException import dsp.errors.RequestRejectedException import dsp.errors.UnexpectedMessageException -import org.knora.webapi.config.AppConfig object ActorUtil { @@ -41,7 +40,6 @@ object ActorUtil { def zio2Message[A]( sender: ActorRef, zioTask: zio.Task[A], - appConfig: AppConfig, log: Logger, runtime: Runtime[Any] ): Unit = diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/isEntityUsed.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/isEntityUsed.scala.txt index 573d0428d0c..32db5ba75dd 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/isEntityUsed.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/isEntityUsed.scala.txt @@ -14,7 +14,7 @@ * @param ignoreRdfSubjectAndObject if true, rdf:subject and rdf:object will be ignored. This is necessary when checking * for references to a resource that is to be erased. *@ -@(entityIri: SmartIri, +@(entityIri: org.knora.webapi.slice.resourceinfo.domain.InternalIri, ignoreKnoraConstraints: Boolean = false, ignoreRdfSubjectAndObject: Boolean = false) @@ -27,7 +27,7 @@ PREFIX knora-base: SELECT DISTINCT ?isUsed WHERE { - BIND(IRI("@entityIri") AS ?entity) + BIND(IRI("@{entityIri.value}") AS ?entity) BIND(true AS ?isUsed) ?s ?p ?entity . diff --git a/webapi/src/main/twirl/org/knora/webapi/queries/sparql/v2/isPropertyUsed.scala.txt b/webapi/src/main/twirl/org/knora/webapi/queries/sparql/v2/isPropertyUsed.scala.txt index 358420f941b..07c3ddbe483 100644 --- a/webapi/src/main/twirl/org/knora/webapi/queries/sparql/v2/isPropertyUsed.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/queries/sparql/v2/isPropertyUsed.scala.txt @@ -3,21 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 *@ -@import org.knora.webapi.IRI - @* - * Checks whether a property is used (i.e., is the property of any statements). + * This query is asking whether there are any instances of a class or subclass that have a specific property. + * I.e. checks whether a property is used, for example is the property of any statements. * - * @param propertyIri the IRI of the property. - * @param ignoreKnoraConstraints if true, knora-base:subjectClassConstraint and knora-base:objectClassConstraint will be ignored. - * This is necessary when modifying the cardinalities of a class. - * @param ignoreRdfSubjectAndObject if true, rdf:subject and rdf:object will be ignored. This is necessary when checking - * for references to a resource that is to be erased. + * @param propertyIri the IRI of the property + * @param classIri the IRI of class to check *@ -@(internalPropertyIri: IRI, - internalClassIri: IRI, - ignoreKnoraConstraints: Boolean = false, - ignoreRdfSubjectAndObject: Boolean = false) + +@( + propertyIri: org.knora.webapi.slice.resourceinfo.domain.InternalIri, + classIri: org.knora.webapi.slice.resourceinfo.domain.InternalIri +) PREFIX rdf: PREFIX rdfs: @@ -26,21 +23,20 @@ PREFIX xsd: PREFIX knora-base: ASK - WHERE { - BIND(IRI("@internalPropertyIri") AS ?property) - BIND(IRI("@internalClassIri") AS ?classIri) - - { - # select all items of type classIri with property ?property. - ?s ?property ?o . - ?s rdf:type ?classIri . - } + BIND(IRI("@{propertyIri.value}") AS ?property) + BIND(IRI("@{classIri.value}") AS ?classIri) - UNION { - # select all items belonging to a subclass of classIri and with property ?property. - ?s ?property ?o . - ?s rdf:type ?class . - ?class rdfs:subClassOf* ?classIri . - } + { + # select all items of type classIri with property ?property. + ?s ?property ?o; + rdf:type ?classIri . + } + UNION + { + # select all items belonging to a subclass of classIri and with property ?property. + ?s ?property ?o ; + rdf:type ?class . + ?class rdfs:subClassOf* ?classIri . + } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/ActorDepsTest.scala b/webapi/src/test/scala/org/knora/webapi/responders/ActorDepsTest.scala new file mode 100644 index 00000000000..5fcf269afa2 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/responders/ActorDepsTest.scala @@ -0,0 +1,26 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.responders + +import akka.actor.Actor +import akka.actor.ActorSystem +import akka.actor.Props +import zio.ULayer +import zio.ZLayer + +import scala.concurrent.duration.DurationInt + +object ActorDepsTest { + + val stub: ULayer[ActorDeps] = ZLayer.succeed { + class StubActor extends Actor { + def receive: Receive = println(_) + } + val system = ActorSystem("test-system") + val ref = system.actorOf(Props[StubActor](), "stub-actor") + ActorDeps(system, ref, 5.seconds) + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/routing/RouteUtilZSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/RouteUtilZSpec.scala index 71fb0ca648a..868b861b928 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/RouteUtilZSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/RouteUtilZSpec.scala @@ -1,3 +1,8 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + package org.knora.webapi.routing import zio.test.Assertion._ diff --git a/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala index 14a97f59864..3fddc7e2ef5 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala @@ -1,3 +1,8 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + package org.knora.webapi.routing.admin import zhttp.http.Cookie import zhttp.http.Headers 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 new file mode 100644 index 00000000000..91f34c1697f --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/CardinalityServiceLiveSpec.scala @@ -0,0 +1,342 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.domain + +import org.apache.jena.query.Dataset +import zio.Random +import zio.Ref +import zio.ZLayer +import zio.test.ZIOSpecDefault +import zio.test._ + +import org.knora.webapi.ApiV2Complex +import org.knora.webapi.InternalSchema +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.v2.responder.ontologymessages.ClassInfoContentV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.OntologyMetadataV2 +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.responders.ActorDepsTest +import org.knora.webapi.responders.v2.ontology.Cache +import org.knora.webapi.responders.v2.ontology.Cache.OntologyCacheData +import org.knora.webapi.slice.ontology.domain.model.Cardinality +import org.knora.webapi.slice.ontology.domain.model.Cardinality.AtLeastOne +import org.knora.webapi.slice.ontology.domain.model.Cardinality.ExactlyOne +import org.knora.webapi.slice.ontology.domain.model.Cardinality.Unbounded +import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne +import org.knora.webapi.slice.ontology.domain.model.Cardinality.allCardinalities +import org.knora.webapi.slice.ontology.domain.service.CardinalityService +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanSetCardinalityCheckResult.BaseClassCheckFailure +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanSetCardinalityCheckResult.CanSetCheckSuccess +import org.knora.webapi.slice.ontology.domain.service.ChangeCardinalityCheckResult.CanSetCardinalityCheckResult.KnoraOntologyCheckFailure +import org.knora.webapi.slice.ontology.repo.service.OntologyCacheFake +import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.slice.resourceinfo.domain.IriTestConstants._ +import org.knora.webapi.store.triplestore.TestDatasetBuilder._ +import org.knora.webapi.store.triplestore.api.TriplestoreServiceFake + +object CardinalityServiceLiveSpec extends ZIOSpecDefault { + + def cardinalitiesGen(cardinalities: Cardinality*): Gen[Any, Cardinality] = Gen.fromZIO { + val candidates: Array[Cardinality] = if (cardinalities != Nil) { cardinalities.toArray } + else { allCardinalities } + val length = candidates.length + if (length == 0) { + throw new IllegalArgumentException() + } else { + Random.nextIntBetween(0, length).map(i => candidates(i)) + } + } + + private object IsPropertyUsedInResourcesTestData { + + private val ontologyAnything = "http://www.knora.org/ontology/0001/anything" + private val ontologyBooks = "http://www.knora.org/ontology/0001/books" + + val classThing: InternalIri = InternalIri(s"$ontologyAnything#Thing") + val classBook: InternalIri = InternalIri(s"$ontologyBooks#Book") + val classTextBook: InternalIri = InternalIri(s"$ontologyBooks#Textbook") + val turtle: String = + s""" + |@prefix rdf: . + |@prefix rdfs: . + | + | + | a <${classThing.value}> ; + | <${KnoraBase.Property.isDeleted.value}> false . + | + | a <${classBook.value}> . + | + |<${classTextBook.value}> rdfs:subClassOf <${classBook.value}> . + | + | + | a <${classTextBook.value}> ; + | <${KnoraBase.Property.isEditable.value}> true . + |""".stripMargin + } + + private object CanWidenCardinalityTestData { + object Gens { + case class TestIris( + ontologyIri: InternalIri, + classIri: InternalIri, + subClassIri: InternalIri, + propertyIri: InternalIri + ) + + val knoraOntologiesGen: Gen[Any, TestIris] = Gen.fromZIO { + val values = Array( + (KnoraBase.Ontology, KnoraBase.Class.Resource, KnoraBase.Class.Annotation, KnoraBase.Property.isDeleted), + ( + KnoraAdmin.Ontology, + KnoraAdmin.Class.Permission, + KnoraAdmin.Class.AdministrativePermission, + KnoraAdmin.Property.belongsToProject + ) + ) + Random + .nextIntBounded(values.length) + .map(values(_)) + .map(iris => TestIris(iris._1, iris._2, iris._3, iris._4)) + } + } + + private val sf: StringFormatter = { StringFormatter.initForTest(); StringFormatter.getGeneralInstance } + + case class DataCreated(subclass: InternalIri, property: InternalIri, data: OntologyCacheData) + + def makeOntologyTestData(cardinality: Cardinality): DataCreated = { + val ontologyIri: InternalIri = InternalIri("http://www.knora.org/ontology/0001/anything") + makeOntologyTestData( + cardinality = cardinality, + propertyIri = InternalIri(s"${ontologyIri.value}#aProperty"), + subClassIri = InternalIri(s"${ontologyIri.value}#aSubClass"), + classIri = InternalIri(s"${ontologyIri.value}#aClass"), + ontologyIri = ontologyIri + ) + } + + def makeOntologyTestData( + cardinality: Cardinality, + ontologyIri: InternalIri, + classIri: InternalIri, + subClassIri: InternalIri, + propertyIri: InternalIri + ): DataCreated = { + val anOntologySmartIri: SmartIri = sf.toSmartIri(ontologyIri.value).toOntologySchema(InternalSchema) + val aClassSmartIri: SmartIri = sf.toSmartIri(classIri.value).toOntologySchema(InternalSchema) + val aSubClassSmartIri: SmartIri = sf.toSmartIri(subClassIri.value).toOntologySchema(InternalSchema) + val aPropertySmartIri: SmartIri = sf.toSmartIri(propertyIri.value).toOntologySchema(InternalSchema) + + val propertyCardinality = makePropertyCardinality(aPropertySmartIri, cardinality) + val classInfo = makeClassInfoContent(aClassSmartIri, directCardinalities = propertyCardinality) + val subClassInfo = makeClassInfoContent( + aSubClassSmartIri, + superClassIris = List(aClassSmartIri), + inheritedCardinalities = propertyCardinality + ) + val ontologyData = makeOntologyData(anOntologySmartIri, classes = classInfo, subClassInfo) + DataCreated(aSubClassSmartIri.toInternalIri, aPropertySmartIri.toInternalIri, ontologyData) + } + + private def makePropertyCardinality( + propertyIri: SmartIri, + cardinality: Cardinality + ): Map[SmartIri, KnoraCardinalityInfo] = Map(propertyIri -> KnoraCardinalityInfo(cardinality.oldCardinality)) + + private def makeClassInfoContent( + classIri: SmartIri, + superClassIris: List[SmartIri] = List.empty, + directCardinalities: Map[SmartIri, KnoraCardinalityInfo] = Map.empty, + inheritedCardinalities: Map[SmartIri, KnoraCardinalityInfo] = Map.empty + ): (SmartIri, ReadClassInfoV2) = + ( + classIri, + ReadClassInfoV2( + ClassInfoContentV2( + classIri = classIri, + ontologySchema = ApiV2Complex, + directCardinalities = directCardinalities + ), + allBaseClasses = superClassIris, + inheritedCardinalities = inheritedCardinalities + ) + ) + + private def makeOntologyData( + ontologyIri: SmartIri, + classes: (SmartIri, ReadClassInfoV2)* + ): Cache.OntologyCacheData = + OntologyCacheFake.emptyData.copy(ontologies = + Map(ontologyIri -> ReadOntologyV2(OntologyMetadataV2(ontologyIri), classes = classes.toMap)) + ) + } + + private val commonLayers = ZLayer.makeSome[Ref[Dataset], CardinalityService with OntologyCacheFake]( + ActorDepsTest.stub, + CardinalityService.layer, + IriConverter.layer, + OntologyCacheFake.emptyCache, + OntologyRepoLive.layer, + StringFormatter.test, + TriplestoreServiceFake.layer + ) + + override def spec: Spec[Any, Throwable] = + suite("CardinalityServiceLive")( + suite("isPropertyUsedInResources should")( + test("given a property is in use by the class => return true") { + val classIri = IsPropertyUsedInResourcesTestData.classThing + val propertyIri = KnoraBase.Property.isDeleted + for { + result <- CardinalityService.isPropertyUsedInResources(classIri, propertyIri) + } yield assertTrue(result) + }, + test("given a property is not used by the class but in a different class => return false") { + val classIri = IsPropertyUsedInResourcesTestData.classBook + val propertyIri = KnoraBase.Property.isDeleted + for { + result <- CardinalityService.isPropertyUsedInResources(classIri, propertyIri) + } yield assertTrue(!result) + }, + test("given a property is never used => return false") { + val classIri = IsPropertyUsedInResourcesTestData.classThing + val propertyIri = KnoraBase.Property.isMainResource + for { + result <- CardinalityService.isPropertyUsedInResources(classIri, propertyIri) + } yield assertTrue(!result) + }, + test("given a property is in use in a subclass => return true") { + val classIri = IsPropertyUsedInResourcesTestData.classTextBook + val propertyIri = KnoraBase.Property.isEditable + for { + result <- CardinalityService.isPropertyUsedInResources(classIri, propertyIri) + } yield assertTrue(result) + } + ).provide(commonLayers, datasetLayerFromTurtle(IsPropertyUsedInResourcesTestData.turtle)), + suite("canSetCardinality")( + test("Any class/property of the Knora admin or base ontologies may never be changed") { + check(CanWidenCardinalityTestData.Gens.knoraOntologiesGen) { iris => + check(cardinalitiesGen()) { cardinality => + val d = CanWidenCardinalityTestData.makeOntologyTestData( + cardinality, + iris.ontologyIri, + iris.classIri, + iris.subClassIri, + iris.propertyIri + ) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- CardinalityService.canSetCardinality(iris.classIri, iris.propertyIri, cardinality) + } yield assertTrue(actual == KnoraOntologyCheckFailure) + } + } + }, + suite(s"Given 'ExactlyOne $ExactlyOne' Cardinality on super class property")( + test( + s""" + |when checking new cardinalities 'AtLeastOne $AtLeastOne', 'Unbounded $Unbounded', 'ZeroOrOne $ZeroOrOne' + |then this is NOT possible""".stripMargin + ) { + check(cardinalitiesGen(AtLeastOne, Unbounded, ZeroOrOne)) { newCardinality => + val d = CanWidenCardinalityTestData.makeOntologyTestData(ExactlyOne) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- CardinalityService.canSetCardinality(d.subclass, d.property, newCardinality) + } yield assertTrue(actual == BaseClassCheckFailure) + } + }, + test( + s""" + |when checking new cardinality 'ExactlyOne $ExactlyOne' + |then this is possible""".stripMargin + ) { + val d = CanWidenCardinalityTestData.makeOntologyTestData(ExactlyOne) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- CardinalityService.canSetCardinality(d.subclass, d.property, ExactlyOne) + } yield assertTrue(actual == CanSetCheckSuccess) + } + ), + suite(s"Given 'AtLeastOne $AtLeastOne' Cardinality on super class property")( + test( + s""" + |when checking new cardinalities 'Unbounded $Unbounded', 'ZeroOrOne $ZeroOrOne' + |then this is NOT possible""".stripMargin + ) { + check(cardinalitiesGen(Unbounded, ZeroOrOne)) { newCardinality => + val d = CanWidenCardinalityTestData.makeOntologyTestData(AtLeastOne) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- + CardinalityService.canSetCardinality(d.subclass, d.property, newCardinality) + } yield assertTrue(actual == BaseClassCheckFailure) + } + }, + test( + s""" + |when checking new cardinalities 'AtLeastOne $AtLeastOne', 'ExactlyOne $ExactlyOne' + |then this is possible""".stripMargin + ) { + check(cardinalitiesGen(AtLeastOne, ExactlyOne)) { newCardinality => + val d = CanWidenCardinalityTestData.makeOntologyTestData(AtLeastOne) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- CardinalityService.canSetCardinality(d.subclass, d.property, newCardinality) + } yield assertTrue(actual == CanSetCheckSuccess) + } + } + ), + suite(s"Given 'ZeroOrOne $ZeroOrOne' Cardinality on super class property")( + test( + s""" + |when checking new cardinalities 'AtLeastOne $AtLeastOne', 'Unbounded $Unbounded' + |then this is NOT possible""".stripMargin + ) { + check(cardinalitiesGen(AtLeastOne, Unbounded)) { newCardinality => + val d = CanWidenCardinalityTestData.makeOntologyTestData(ZeroOrOne) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- CardinalityService.canSetCardinality(d.subclass, d.property, newCardinality) + } yield assertTrue(actual == BaseClassCheckFailure) + } + }, + test( + s""" + |when checking new cardinalities 'ExactlyOne $ExactlyOne', 'ZeroOrOne $ZeroOrOne' + |then this is possible""".stripMargin + ) { + check(cardinalitiesGen(ExactlyOne, ZeroOrOne)) { newCardinality => + val d = CanWidenCardinalityTestData.makeOntologyTestData(ZeroOrOne) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- CardinalityService.canSetCardinality(d.subclass, d.property, newCardinality) + } yield assertTrue(actual == CanSetCheckSuccess) + } + } + ), + test( + s""" + |Given 'Unbounded $Unbounded' Cardinality on super class property' + |when checking all cardinalities + |then this is always possible""".stripMargin + ) { + check(cardinalitiesGen()) { newCardinality => + val d = CanWidenCardinalityTestData.makeOntologyTestData(Unbounded) + for { + _ <- OntologyCacheFake.set(d.data) + actual <- CardinalityService.canSetCardinality(d.subclass, d.property, newCardinality) + } yield assertTrue(actual == CanSetCheckSuccess) + } + } + ).provide(commonLayers, emptyDataSet) + ) + +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/model/CardinalitySpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/model/CardinalitySpec.scala new file mode 100644 index 00000000000..e318da8011b --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/domain/model/CardinalitySpec.scala @@ -0,0 +1,80 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.domain.model + +import zio.Random +import zio.Scope +import zio.test._ + +import org.knora.webapi.slice.ontology.domain.model.Cardinality._ + +object CardinalitySpec extends ZIOSpecDefault { + val cardinalityGen: Gen[Any, Cardinality] = { + val list = Array(AtLeastOne, ExactlyOne, ZeroOrOne, Unbounded) + Gen.fromZIO(for { + random <- Random.nextIntBetween(0, list.length) + } yield list(random)) + } + + val spec: Spec[TestEnvironment with Scope, Nothing] = suite("CardinalitySpec")( + suite("Cardinality to String")( + test("lower bound only") { + assertTrue(AtLeastOne.toString == "1-n") + }, + test("different lower and upper bound ") { + assertTrue(ZeroOrOne.toString == "0-1") + }, + test("same upper and lower bound") { + assertTrue(ExactlyOne.toString == "1") + } + ), + suite("Cardinality isStricterThan")( + test("Same cardinality is never stricter") { + check(cardinalityGen)(c => assertTrue(!c.isStricterThan(c))) + }, + suite(s"Unbounded $Unbounded")( + test(s"'$AtLeastOne' is stricter than $Unbounded") { + assertTrue(AtLeastOne.isStricterThan(Unbounded)) + }, + test(s"'$ExactlyOne is stricter than $Unbounded") { + assertTrue(ExactlyOne.isStricterThan(Unbounded)) + }, + test(s"'$ZeroOrOne' is stricter than $Unbounded") { + assertTrue(ZeroOrOne.isStricterThan(Unbounded)) + }, + test(s"'$Unbounded' is NOT stricter than any other") { + check(cardinalityGen) { other => + assertTrue(!Unbounded.isStricterThan(other)) + } + } + ), + suite(s"AtLeastOne $AtLeastOne")( + test(s"'$AtLeastOne' is NOT stricter than $ExactlyOne") { + assertTrue(!AtLeastOne.isStricterThan(ExactlyOne)) + }, + test(s"'$AtLeastOne' is stricter than $ZeroOrOne") { + assertTrue(AtLeastOne.isStricterThan(ZeroOrOne)) + } + ), + suite(s"ExactlyOne $ExactlyOne")( + test(s"'$ExactlyOne' is stricter than $AtLeastOne") { + assertTrue(ExactlyOne.isStricterThan(AtLeastOne)) + }, + test(s"'$ExactlyOne' is stricter than $ZeroOrOne") { + assertTrue(ExactlyOne.isStricterThan(ZeroOrOne)) + } + ), + suite(s"ExactlyOne $ZeroOrOne")( + test(s"'$ZeroOrOne' is stricter than $AtLeastOne") { + assertTrue(ZeroOrOne.isStricterThan(AtLeastOne)) + }, + test(s"'$ZeroOrOne' is NOT stricter than $ExactlyOne") { + assertTrue(!ZeroOrOne.isStricterThan(ExactlyOne)) + } + ) + ) + ) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/OntologyCacheFakeSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/OntologyCacheFakeSpec.scala new file mode 100644 index 00000000000..9ea59cc8287 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/OntologyCacheFakeSpec.scala @@ -0,0 +1,34 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.repo + +import zio.test.ZIOSpecDefault +import zio.test._ + +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.slice.ontology.repo.service.OntologyCache +import org.knora.webapi.slice.ontology.repo.service.OntologyCacheFake +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +object OntologyCacheFakeSpec extends ZIOSpecDefault { + val spec: Spec[Any, Throwable] = suite("OntologyCacheFake")( + suite("with empty cache")(test("should return empty") { + for { + actual <- OntologyCache.get + } yield assertTrue(actual == OntologyCacheFake.emptyData) + }).provide(OntologyCacheFake.emptyCache), + suite("with empty cache when setting new data")(test("should return set cache") { + val somePropertyIri = InternalIri("http://www.knora.org/ontology/knora-base#mappingHasXMLAttribute") + for { + someIri <- IriConverter.asInternalSmartIri(somePropertyIri) + newData = OntologyCacheFake.emptyData.copy(standoffProperties = Set(someIri)) + _ <- OntologyCacheFake.set(newData) + actual <- OntologyCache.get + } yield assertTrue(actual == newData) + }).provide(OntologyCacheFake.emptyCache, IriConverter.layer, StringFormatter.test) + ) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCacheFake.scala b/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCacheFake.scala new file mode 100644 index 00000000000..5a8fadd6e14 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyCacheFake.scala @@ -0,0 +1,36 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.repo.service +import zio.Ref +import zio.Task +import zio.UIO +import zio.ZIO +import zio.ZLayer + +import org.knora.webapi.responders.v2.ontology.Cache +import org.knora.webapi.responders.v2.ontology.Cache.OntologyCacheData + +case class OntologyCacheFake(ref: Ref[OntologyCacheData]) extends OntologyCache { + override def get: Task[Cache.OntologyCacheData] = ref.get + def set(data: OntologyCacheData): UIO[Unit] = ref.set(data) +} + +object OntologyCacheFake { + + def set(data: OntologyCacheData): ZIO[OntologyCacheFake, Nothing, Unit] = + ZIO.service[OntologyCacheFake].flatMap(_.set(data)) + + def withCache(data: OntologyCacheData): ZLayer[Any, Nothing, OntologyCacheFake] = ZLayer.fromZIO { + for { + ref <- Ref.make[OntologyCacheData](data) + } yield OntologyCacheFake(ref) + } + + val emptyData: OntologyCacheData = + OntologyCacheData(Map.empty, Map.empty, Map.empty, Map.empty, Map.empty, Map.empty, Map.empty, Map.empty, Set.empty) + + val emptyCache: ZLayer[Any, Nothing, OntologyCacheFake] = withCache(emptyData) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLiveSpec.scala new file mode 100644 index 00000000000..7ae2ee5dab3 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLiveSpec.scala @@ -0,0 +1,86 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.ontology.repo.service + +import zio.Scope +import zio.test.ZIOSpecDefault +import zio.test._ + +import org.knora.webapi.ApiV2Complex +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.v2.responder.ontologymessages.ClassInfoContentV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.OntologyMetadataV2 +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.service.OntologyRepo +import org.knora.webapi.slice.resourceinfo.domain.InternalIri +import org.knora.webapi.slice.resourceinfo.domain.IriConverter + +object OntologyRepoLiveSpec extends ZIOSpecDefault { + + private val sf = { StringFormatter.initForTest(); StringFormatter.getGeneralInstance } + private val anUnknownInternalOntologyIri = InternalIri("http://www.knora.org/ontology/0001/anything") + private val anUnknownClassIri = InternalIri("http://www.knora.org/ontology/0001/anything#Thing") + + private val aKnownClassIri: InternalIri = InternalIri("http://www.knora.org/ontology/0001/gizmo#Gizmo") + private val aKnownClassSmartIri: SmartIri = sf.toSmartIri(aKnownClassIri.value) + private val ontologySmartIri: SmartIri = aKnownClassSmartIri.getOntologyFromEntity + + val spec: Spec[TestEnvironment with Scope, Any] = + suite("OntologyRepoLive")( + suite("findOntologyBy(InternalIri)")( + test("when searching for unknown iri => return None") { + for { + actual <- OntologyRepo.findById(anUnknownInternalOntologyIri) + } yield assertTrue(actual.isEmpty) + }, + test("when searching for known iri => return Some(ReadOntology)") { + val ontologyData = ReadOntologyV2(OntologyMetadataV2(ontologySmartIri)) + val cacheData = OntologyCacheFake.emptyData.copy(ontologies = Map(ontologySmartIri -> ontologyData)) + for { + _ <- OntologyCacheFake.set(cacheData) + actual <- OntologyRepo.findById(ontologySmartIri.toInternalIri) + } yield assertTrue(actual.contains(ontologyData)) + } + ), + suite("findClassBy(InternalIri)")( + test("when searching for a known iri => return Some(ReadClassInfoV2])") { + val classData = ReadClassInfoV2( + ClassInfoContentV2(aKnownClassSmartIri, ontologySchema = ApiV2Complex), + allBaseClasses = List.empty + ) + val ontologyData = + ReadOntologyV2(OntologyMetadataV2(ontologySmartIri), classes = Map(aKnownClassSmartIri -> classData)) + val cacheData = OntologyCacheFake.emptyData.copy(ontologies = Map(ontologySmartIri -> ontologyData)) + for { + _ <- OntologyCacheFake.set(cacheData) + actual <- OntologyRepo.findClassBy(aKnownClassIri) + } yield assertTrue(actual.contains(classData)) + }, + test("when searching for unknown iri => return None") { + for { + actual <- OntologyRepo.findClassBy(anUnknownClassIri) + } yield assertTrue(actual.isEmpty) + } + ), + suite("findAll()")( + test("given cache is Empty => return empty List") { + for { + actual <- OntologyRepo.findAll() + } yield assertTrue(actual.isEmpty) + }, + test("given cache has an ontology => return List of ontologies") { + val ontologyData = ReadOntologyV2(OntologyMetadataV2(ontologySmartIri)) + val cacheData = OntologyCacheFake.emptyData.copy(ontologies = Map(ontologySmartIri -> ontologyData)) + for { + _ <- OntologyCacheFake.set(cacheData) + actual <- OntologyRepo.findAll() + } yield assertTrue(actual == List(ontologyData)) + } + ) + ).provide(OntologyRepoLive.layer, OntologyCacheFake.emptyCache, IriConverter.layer, StringFormatter.test) +} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/domain/IriTestConstants.scala b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/domain/IriTestConstants.scala new file mode 100644 index 00000000000..8be2fd3d5b8 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/domain/IriTestConstants.scala @@ -0,0 +1,35 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.resourceinfo.domain + +object IriTestConstants { + private def makeEntity(ontologyIri: InternalIri, entityName: String) = + InternalIri(s"${ontologyIri.value}#$entityName") + + object KnoraBase { + val Ontology: InternalIri = InternalIri("http://www.knora.org/ontology/knora-base") + object Property { + val isDeleted: InternalIri = makeEntity(KnoraBase.Ontology, "isDeleted") + val isEditable: InternalIri = makeEntity(KnoraBase.Ontology, "isEditable") + val isMainResource: InternalIri = makeEntity(KnoraBase.Ontology, "isMainResource") + } + object Class { + val Annotation: InternalIri = makeEntity(KnoraBase.Ontology, "Annotation") + val Resource: InternalIri = makeEntity(KnoraBase.Ontology, "Resource") + } + } + object KnoraAdmin { + val Ontology: InternalIri = InternalIri("http://www.knora.org/ontology/knora-admin") + object Property { + val belongsToProject: InternalIri = makeEntity(KnoraAdmin.Ontology, "belongsToProject") + } + object Class { + val AdministrativePermission: InternalIri = makeEntity(KnoraAdmin.Ontology, "AdministrativePermission") + val Institution: InternalIri = makeEntity(KnoraAdmin.Ontology, "Institution") + val Permission: InternalIri = makeEntity(KnoraAdmin.Ontology, "Permission") + } + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/store/triplestore/TestDatasetBuilder.scala b/webapi/src/test/scala/org/knora/webapi/store/triplestore/TestDatasetBuilder.scala new file mode 100644 index 00000000000..1932a60289f --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/triplestore/TestDatasetBuilder.scala @@ -0,0 +1,45 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.store.triplestore + +import org.apache.jena.query.Dataset +import org.apache.jena.query.DatasetFactory +import org.apache.jena.query.ReadWrite +import org.apache.jena.rdf.model.Model +import zio.Ref +import zio.Task +import zio.TaskLayer +import zio.UIO +import zio.ZIO +import zio.ZLayer + +import java.io.StringReader + +object TestDatasetBuilder { + + def readToModel(turtle: String)(model: Model): Model = model.read(new StringReader(turtle), null, "TTL") + + def transactionalWrite(change: Model => Model)(ds: Dataset): Task[Dataset] = ZIO.attempt { + ds.begin(ReadWrite.WRITE) + try { + change apply ds.getDefaultModel + ds.commit() + } finally { + ds.end() + } + ds + } + + val createTxnMemDataset: UIO[Dataset] = ZIO.succeed(DatasetFactory.createTxnMem()) + + def datasetFromTurtle(turtle: String): Task[Dataset] = + createTxnMemDataset.flatMap(transactionalWrite(readToModel(turtle))) + + def asLayer(ds: Task[Dataset]): TaskLayer[Ref[Dataset]] = ZLayer.fromZIO(ds.flatMap(Ref.make[Dataset](_))) + + def datasetLayerFromTurtle(turtle: String): TaskLayer[Ref[Dataset]] = asLayer(datasetFromTurtle(turtle)) + val emptyDataSet: TaskLayer[Ref[Dataset]] = datasetLayerFromTurtle("") +} diff --git a/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFake.scala b/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFake.scala new file mode 100644 index 00000000000..a62c14469b9 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFake.scala @@ -0,0 +1,147 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.store.triplestore.api +import org.apache.jena.query.Dataset +import org.apache.jena.query.QueryExecution +import org.apache.jena.query.QueryExecutionFactory +import org.apache.jena.query.QuerySolution +import org.apache.jena.query.ResultSet +import zio.Ref +import zio.Scope +import zio.UIO +import zio.ZIO +import zio.ZLayer + +import java.nio.file.Path +import scala.jdk.CollectionConverters.CollectionHasAsScala +import scala.jdk.CollectionConverters.IteratorHasAsScala + +import org.knora.webapi.IRI +import org.knora.webapi.messages.store.triplestoremessages.CheckTriplestoreResponse +import org.knora.webapi.messages.store.triplestoremessages.DropAllRepositoryContentACK +import org.knora.webapi.messages.store.triplestoremessages.DropDataGraphByGraphACK +import org.knora.webapi.messages.store.triplestoremessages.FileWrittenResponse +import org.knora.webapi.messages.store.triplestoremessages.InsertGraphDataContentResponse +import org.knora.webapi.messages.store.triplestoremessages.InsertTriplestoreContentACK +import org.knora.webapi.messages.store.triplestoremessages.NamedGraphDataResponse +import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject +import org.knora.webapi.messages.store.triplestoremessages.RepositoryUploadedResponse +import org.knora.webapi.messages.store.triplestoremessages.ResetRepositoryContentACK +import org.knora.webapi.messages.store.triplestoremessages.SparqlAskResponse +import org.knora.webapi.messages.store.triplestoremessages.SparqlConstructRequest +import org.knora.webapi.messages.store.triplestoremessages.SparqlConstructResponse +import org.knora.webapi.messages.store.triplestoremessages.SparqlExtendedConstructRequest +import org.knora.webapi.messages.store.triplestoremessages.SparqlExtendedConstructResponse +import org.knora.webapi.messages.store.triplestoremessages.SparqlUpdateResponse +import org.knora.webapi.messages.util.rdf.QuadFormat +import org.knora.webapi.messages.util.rdf.SparqlSelectResult +import org.knora.webapi.messages.util.rdf.SparqlSelectResultBody +import org.knora.webapi.messages.util.rdf.SparqlSelectResultHeader +import org.knora.webapi.messages.util.rdf.VariableResultsRow + +final case class TriplestoreServiceFake(datasetRef: Ref[Dataset]) extends TriplestoreService { + + override def doSimulateTimeout(): UIO[SparqlSelectResult] = ??? + + override def sparqlHttpSelect( + sparql: String, + simulateTimeout: Boolean, + isGravsearch: Boolean + ): UIO[SparqlSelectResult] = { + require(!simulateTimeout, "`simulateTimeout` parameter is not supported by fake implementation yet") + require(!isGravsearch, "`isGravsearch` parameter is not supported by fake implementation yet") + + ZIO.scoped(execSelect(sparql).map(toSparqlSelectResult)).orDie + } + + private def execSelect(query: String): ZIO[Any with Scope, Throwable, ResultSet] = { + def executeQuery(qExec: QueryExecution) = ZIO.attempt(qExec.execSelect) + def closeResultSet(rs: ResultSet) = ZIO.succeed(rs.close()) + getQueryExecution(query).flatMap(qExec => ZIO.acquireRelease(executeQuery(qExec))(closeResultSet)) + } + + private def getQueryExecution(query: String): ZIO[Any with Scope, Throwable, QueryExecution] = { + def acquire(query: String, ds: Dataset) = ZIO.attempt(QueryExecutionFactory.create(query, ds)) + def release(qExec: QueryExecution) = ZIO.succeed(qExec.close()) + datasetRef.get.flatMap(ds => ZIO.acquireRelease(acquire(query, ds))(release)) + } + + private def toSparqlSelectResult(resultSet: ResultSet): SparqlSelectResult = { + val header = SparqlSelectResultHeader(resultSet.getResultVars.asScala.toList) + val resultBody = getSelectResultBody(resultSet) + SparqlSelectResult(header, resultBody) + } + + private def getSelectResultBody(resultSet: ResultSet): SparqlSelectResultBody = { + val rows: Seq[VariableResultsRow] = resultSet.asScala + .foldRight(List.empty[VariableResultsRow])((solution, list) => list.prepended(asVariableResultsRow(solution))) + SparqlSelectResultBody(rows) + } + + private def asVariableResultsRow(solution: QuerySolution): VariableResultsRow = { + val keyValueMap = solution.varNames.asScala.map { key => + val node = solution.get(key).asNode + val value: String = // do not include datatype in string if node is a literal + if (node.isLiteral) { node.getLiteralLexicalForm } + else { node.toString } + key -> value + }.toMap + VariableResultsRow(keyValueMap) + } + + override def sparqlHttpAsk(query: String): UIO[SparqlAskResponse] = + ZIO.scoped(getQueryExecution(query).map(_.execAsk())).map(SparqlAskResponse).orDie + + override def sparqlHttpConstruct(sparqlConstructRequest: SparqlConstructRequest): UIO[SparqlConstructResponse] = ??? + + override def sparqlHttpExtendedConstruct( + sparqlExtendedConstructRequest: SparqlExtendedConstructRequest + ): UIO[SparqlExtendedConstructResponse] = ??? + + override def sparqlHttpConstructFile( + sparql: String, + graphIri: IRI, + outputFile: Path, + outputFormat: QuadFormat + ): UIO[FileWrittenResponse] = ??? + + override def sparqlHttpUpdate(sparqlUpdate: String): UIO[SparqlUpdateResponse] = ??? + + override def sparqlHttpGraphFile( + graphIri: IRI, + outputFile: Path, + outputFormat: QuadFormat + ): UIO[FileWrittenResponse] = ??? + + override def sparqlHttpGraphData(graphIri: IRI): UIO[NamedGraphDataResponse] = ??? + + override def resetTripleStoreContent( + rdfDataObjects: List[RdfDataObject], + prependDefaults: Boolean + ): UIO[ResetRepositoryContentACK] = ??? + + override def dropAllTriplestoreContent(): UIO[DropAllRepositoryContentACK] = ??? + + override def dropDataGraphByGraph(): UIO[DropDataGraphByGraphACK] = ??? + + override def insertDataIntoTriplestore( + rdfDataObjects: List[RdfDataObject], + prependDefaults: Boolean + ): UIO[InsertTriplestoreContentACK] = ??? + + override def checkTriplestore(): UIO[CheckTriplestoreResponse] = ??? + + override def downloadRepository(outputFile: Path): UIO[FileWrittenResponse] = ??? + + override def uploadRepository(inputFile: Path): UIO[RepositoryUploadedResponse] = ??? + + override def insertDataGraphRequest(graphContent: String, graphName: String): UIO[InsertGraphDataContentResponse] = + ??? +} + +object TriplestoreServiceFake { + val layer: ZLayer[Ref[Dataset], Nothing, TriplestoreService] = ZLayer.fromFunction(TriplestoreServiceFake.apply _) +} diff --git a/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFakeSpec.scala b/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFakeSpec.scala new file mode 100644 index 00000000000..26acabab428 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/store/triplestore/api/TriplestoreServiceFakeSpec.scala @@ -0,0 +1,100 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.store.triplestore.api +import zio.test._ + +import org.knora.webapi.messages.util.rdf._ +import org.knora.webapi.store.triplestore.TestDatasetBuilder.datasetLayerFromTurtle + +object TriplestoreServiceFakeSpec extends ZIOSpecDefault { + + private val testDataSet = + """ + |@prefix rdf: . + |@prefix anything: . + | + | a anything:Thing . + | + |""".stripMargin + + val spec = suite("TriplestoreServiceFake")( + suite("sparqlHttpAsk")( + test("should return true if thing exists") { + val query = """ + |PREFIX rdf: + |PREFIX anything: + | + |ASK WHERE { + | a anything:Thing . + |} + |""".stripMargin + for { + result <- TriplestoreService.sparqlHttpAsk(query) + } yield assertTrue(result.result) + }, + test("should return false if thing dose not exist") { + val query = """ + |PREFIX rdf: + |PREFIX anything: + | + |ASK WHERE { + | a anything:Thing . + |} + |""".stripMargin + for { + result <- TriplestoreService.sparqlHttpAsk(query) + } yield assertTrue(!result.result) + } + ), + suite("sparqlHttpSelect")( + test("not find non-existing thing") { + val query = """ + |PREFIX rdf: + | + |SELECT ?p ?o + |WHERE { + | ?p ?o. + |} + |""".stripMargin + for { + result <- TriplestoreService.sparqlHttpSelect(query) + } yield assertTrue( + result == SparqlSelectResult( + SparqlSelectResultHeader(List("p", "o")), + SparqlSelectResultBody(List()) + ) + ) + }, + test("find an existing thing") { + val query = """ + |PREFIX rdf: + | + |SELECT ?p ?o + |WHERE { + | ?p ?o. + |} + |""".stripMargin + for { + result <- TriplestoreService.sparqlHttpSelect(query) + } yield assertTrue( + result == SparqlSelectResult( + SparqlSelectResultHeader(List("p", "o")), + SparqlSelectResultBody( + List( + VariableResultsRow( + Map( + "p" -> "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + "o" -> "http://www.knora.org/ontology/0001/anything#Thing" + ) + ) + ) + ) + ) + ) + } + ) + ).provide(TriplestoreServiceFake.layer, datasetLayerFromTurtle(testDataSet)) +}