diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 44d67771b06..4c91a6b463f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -44,6 +44,7 @@ object Dependencies { // zio-test and friends val zioTest = "dev.zio" %% "zio-test" % ZioVersion val zioTestSbt = "dev.zio" %% "zio-test-sbt" % ZioVersion + val zioMock = "dev.zio" %% "zio-mock" % "1.0.0-RC9" // akka val akkaActor = "com.typesafe.akka" %% "akka-actor" % AkkaActorVersion // Scala 3 compatible @@ -127,7 +128,7 @@ object Dependencies { zioTestSbt ).map(_ % IntegrationTest) - val webapiTestDependencies = Seq(zioTest, zioTestSbt).map(_ % Test) + val webapiTestDependencies = Seq(zioTest, zioTestSbt, zioMock).map(_ % Test) val webapiDependencies = Seq( akkaActor, diff --git a/webapi/src/it/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/it/scala/org/knora/webapi/CoreSpec.scala index 8c9d6c67f3a..a8e9f694c74 100644 --- a/webapi/src/it/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/CoreSpec.scala @@ -9,22 +9,23 @@ import akka.actor import akka.testkit.ImplicitSender import akka.testkit.TestKitBase import com.typesafe.scalalogging.Logger + import org.knora.webapi.config.AppConfig import org.knora.webapi.core.AppRouter import org.knora.webapi.core.AppServer import org.knora.webapi.core.TestStartupUtils import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.util.ResponderData -import org.knora.webapi.store.cache.settings.CacheServiceSettings import org.knora.webapi.util.LogAspect import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import zio._ import zio.logging.backend.SLF4J - import scala.concurrent.ExecutionContext +import org.knora.webapi.responders.ActorDeps + abstract class CoreSpec extends AnyWordSpec with TestKitBase @@ -87,9 +88,8 @@ abstract class CoreSpec val appActor = router.ref // needed by some tests - val appConfig = config - val cacheServiceSettings = new CacheServiceSettings(appConfig) - val responderData = ResponderData(system, appActor, appConfig, cacheServiceSettings) + val appConfig = config + val responderData = ResponderData(ActorDeps(system, appActor, appConfig.defaultTimeoutAsDuration), appConfig) final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ 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 49251584a8e..f026029a700 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -11,6 +11,9 @@ import zio.ZLayer import org.knora.webapi.auth.JWTService import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.responders.ActorDeps +import org.knora.webapi.responders.ActorToZioBridge +import org.knora.webapi.responders.admin.ProjectsService import org.knora.webapi.routing.ApiRoutes import org.knora.webapi.routing.admin.ProjectsRouteZ import org.knora.webapi.slice.resourceinfo.api.ResourceInfoRoute @@ -54,7 +57,9 @@ object LayersLive { */ val dspLayersLive: ULayer[DspEnvironmentLive] = ZLayer.make[DspEnvironmentLive]( + ActorDeps.layer, ActorSystem.layer, + ActorToZioBridge.live, ApiRoutes.layer, AppConfig.live, AppRouter.layer, @@ -67,6 +72,7 @@ object LayersLive { IriConverter.layer, JWTService.layer, ProjectsRouteZ.layer, + ProjectsService.layer, RepositoryUpdater.layer, ResourceInfoRepo.layer, ResourceInfoRoute.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 4ab975aa4aa..0a61834c8c1 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 @@ -6,7 +6,6 @@ package org.knora.webapi.core.actors import akka.actor.Actor -import akka.actor.ActorSystem import com.typesafe.scalalogging.Logger import scala.concurrent.ExecutionContext @@ -39,6 +38,7 @@ import org.knora.webapi.messages.v2.responder.resourcemessages.ResourcesResponde import org.knora.webapi.messages.v2.responder.searchmessages.SearchResponderRequestV2 import org.knora.webapi.messages.v2.responder.standoffmessages.StandoffResponderRequestV2 import org.knora.webapi.messages.v2.responder.valuemessages.ValuesResponderRequestV2 +import org.knora.webapi.responders.ActorDeps import org.knora.webapi.responders.admin.GroupsResponderADM import org.knora.webapi.responders.admin.ListsResponderADM import org.knora.webapi.responders.admin.PermissionsResponderADM @@ -67,7 +67,7 @@ import org.knora.webapi.store.iiif.IIIFServiceManager import org.knora.webapi.store.triplestore.TriplestoreServiceManager import org.knora.webapi.util.ActorUtil -class RoutingActor( +final case class RoutingActor( cacheServiceManager: CacheServiceManager, iiifServiceManager: IIIFServiceManager, triplestoreManager: TriplestoreServiceManager, @@ -75,51 +75,39 @@ class RoutingActor( runtime: zio.Runtime[Any] ) extends Actor { - implicit val system: ActorSystem = context.system - val log: Logger = Logger(this.getClass) - - /** - * The Cache Service's configuration. - */ - implicit val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) - - /** - * Provides the default global execution context - */ - implicit val executionContext: ExecutionContext = context.dispatcher - - /** - * Data used in responders. - */ - val responderData: ResponderData = ResponderData(system, self, appConfig, cacheServiceSettings) + private val log: Logger = Logger(this.getClass) + private val actorDeps: ActorDeps = ActorDeps(context.system, self, appConfig.defaultTimeoutAsDuration) + private val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) + private val responderData: ResponderData = ResponderData(actorDeps, appConfig) + private implicit val executionContext: ExecutionContext = actorDeps.executionContext // V1 responders - val ckanResponderV1: CkanResponderV1 = new CkanResponderV1(responderData) - val resourcesResponderV1: ResourcesResponderV1 = new ResourcesResponderV1(responderData) - val valuesResponderV1: ValuesResponderV1 = new ValuesResponderV1(responderData) - val standoffResponderV1: StandoffResponderV1 = new StandoffResponderV1(responderData) - val usersResponderV1: UsersResponderV1 = new UsersResponderV1(responderData) - val listsResponderV1: ListsResponderV1 = new ListsResponderV1(responderData) - val searchResponderV1: SearchResponderV1 = new SearchResponderV1(responderData) - val ontologyResponderV1: OntologyResponderV1 = new OntologyResponderV1(responderData) - val projectsResponderV1: ProjectsResponderV1 = new ProjectsResponderV1(responderData) + private val ckanResponderV1: CkanResponderV1 = new CkanResponderV1(responderData) + private val resourcesResponderV1: ResourcesResponderV1 = new ResourcesResponderV1(responderData) + private val valuesResponderV1: ValuesResponderV1 = new ValuesResponderV1(responderData) + private val standoffResponderV1: StandoffResponderV1 = new StandoffResponderV1(responderData) + private val usersResponderV1: UsersResponderV1 = new UsersResponderV1(responderData) + private val listsResponderV1: ListsResponderV1 = new ListsResponderV1(responderData) + private val searchResponderV1: SearchResponderV1 = new SearchResponderV1(responderData) + private val ontologyResponderV1: OntologyResponderV1 = new OntologyResponderV1(responderData) + private val projectsResponderV1: ProjectsResponderV1 = ProjectsResponderV1(actorDeps) // V2 responders - val ontologiesResponderV2: OntologyResponderV2 = new OntologyResponderV2(responderData) - val searchResponderV2: SearchResponderV2 = new SearchResponderV2(responderData) - val resourcesResponderV2: ResourcesResponderV2 = new ResourcesResponderV2(responderData) - val valuesResponderV2: ValuesResponderV2 = new ValuesResponderV2(responderData) - val standoffResponderV2: StandoffResponderV2 = new StandoffResponderV2(responderData) - val listsResponderV2: ListsResponderV2 = new ListsResponderV2(responderData) + private val ontologiesResponderV2: OntologyResponderV2 = new OntologyResponderV2(responderData) + private val searchResponderV2: SearchResponderV2 = new SearchResponderV2(responderData) + private val resourcesResponderV2: ResourcesResponderV2 = new ResourcesResponderV2(responderData) + private val valuesResponderV2: ValuesResponderV2 = new ValuesResponderV2(responderData) + private val standoffResponderV2: StandoffResponderV2 = new StandoffResponderV2(responderData) + private val listsResponderV2: ListsResponderV2 = new ListsResponderV2(responderData) // Admin responders - val groupsResponderADM: GroupsResponderADM = new GroupsResponderADM(responderData) - val listsResponderADM: ListsResponderADM = new ListsResponderADM(responderData) - val permissionsResponderADM: PermissionsResponderADM = new PermissionsResponderADM(responderData) - val projectsResponderADM: ProjectsResponderADM = new ProjectsResponderADM(responderData) - val storeResponderADM: StoresResponderADM = new StoresResponderADM(responderData) - val usersResponderADM: UsersResponderADM = new UsersResponderADM(responderData) - val sipiRouterADM: SipiResponderADM = new SipiResponderADM(responderData) + private val groupsResponderADM: GroupsResponderADM = new GroupsResponderADM(responderData) + private val listsResponderADM: ListsResponderADM = new ListsResponderADM(responderData) + private val permissionsResponderADM: PermissionsResponderADM = new PermissionsResponderADM(responderData) + private val projectsResponderADM: ProjectsResponderADM = ProjectsResponderADM(actorDeps, cacheServiceSettings) + private val storeResponderADM: StoresResponderADM = new StoresResponderADM(responderData) + private val usersResponderADM: UsersResponderADM = new UsersResponderADM(responderData) + private val sipiRouterADM: SipiResponderADM = new SipiResponderADM(responderData) def receive: Receive = { @@ -183,5 +171,4 @@ class RoutingActor( s"RoutingActor received an unexpected message $other of type ${other.getClass.getCanonicalName}" ) } - } diff --git a/webapi/src/main/scala/org/knora/webapi/instrumentation/InstrumentationSupport.scala b/webapi/src/main/scala/org/knora/webapi/instrumentation/InstrumentationSupport.scala index 06f48043a53..fae7dc4b3ea 100644 --- a/webapi/src/main/scala/org/knora/webapi/instrumentation/InstrumentationSupport.scala +++ b/webapi/src/main/scala/org/knora/webapi/instrumentation/InstrumentationSupport.scala @@ -54,14 +54,6 @@ trait InstrumentationSupport { } } - // def counter(name: String) = Kamon.metrics.counter(name) - // def minMaxCounter(name: String) = Kamon.metrics.minMaxCounter(name) - // def time[A](name: String)(thunk: => A) = Latency.measure(Kamon.metrics.histogram(name))(thunk) - // def traceFuture[A](name:String)(future: => Future[A]):Future[A] = - // Tracer.withContext(Kamon.tracer.newContext(name)) { - // future.andThen { case completed ⇒ Tracer.currentContext.finish() }(SameThreadExecutionContext) - // } - /** * Based on the current class name, create a logger with the name in the * form 'M-ClassName', e.g., 'M-RedisManager'. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderData.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderData.scala index de158cec61b..bcdf754ba2a 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderData.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ResponderData.scala @@ -7,20 +7,25 @@ package org.knora.webapi.messages.util import akka.actor.ActorRef import akka.actor.ActorSystem +import akka.util.Timeout + +import scala.concurrent.ExecutionContext import org.knora.webapi.config.AppConfig +import org.knora.webapi.responders.ActorDeps import org.knora.webapi.store.cache.settings.CacheServiceSettings /** * Data needed to be passed to each responder. * - * @param system the actor system. - * @param appActor the main application actor. - * @param cacheServiceSettings the cache service part of the settings. + * @param actorDeps all dependencies necessary for interacting with the [[org.knora.webapi.core.actors.RoutingActor]] + * @param appConfig the application configuration for creating the [[CacheServiceSettings]] */ -case class ResponderData( - system: ActorSystem, - appActor: ActorRef, - appConfig: AppConfig, - cacheServiceSettings: CacheServiceSettings -) +case class ResponderData(actorDeps: ActorDeps, appConfig: AppConfig) { + val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) + + val appActor: ActorRef = actorDeps.appActor + val executionContext: ExecutionContext = actorDeps.executionContext + val system: ActorSystem = actorDeps.system + val timeout: Timeout = actorDeps.timeout +} diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala index 193117465e9..fc0747af723 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala @@ -7,7 +7,6 @@ package org.knora.webapi.messages.util.search.gravsearch.types import akka.actor.ActorRef -import scala.concurrent.ExecutionContext import scala.concurrent.Future import dsp.errors.GravsearchException @@ -16,7 +15,6 @@ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.search._ -import org.knora.webapi.settings.KnoraDispatchers /** * Runs Gravsearch type inspection using one or more type inspector implementations. @@ -30,8 +28,7 @@ class GravsearchTypeInspectionRunner( responderData: ResponderData, inferTypes: Boolean = true ) { - private implicit val executionContext: ExecutionContext = - responderData.system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) + private implicit val executionContext = responderData.actorDeps.executionContext // If inference was requested, construct an inferring type inspector. private val maybeInferringTypeInspector: Option[GravsearchTypeInspector] = if (inferTypes) { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspector.scala index dcf8a6bab87..d749dc07a94 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspector.scala @@ -14,7 +14,6 @@ import scala.concurrent.Future import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.search.WhereClause -import org.knora.webapi.settings.KnoraDispatchers /** * An trait whose implementations can get type information from a parsed Gravsearch query in different ways. @@ -29,10 +28,9 @@ abstract class GravsearchTypeInspector( responderData: ResponderData ) { - protected val system: ActorSystem = responderData.system - protected implicit val executionContext: ExecutionContext = - system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) - protected implicit val timeout: Timeout = responderData.appConfig.defaultTimeoutAsDuration + protected val system: ActorSystem = responderData.system + protected implicit val executionContext: ExecutionContext = responderData.executionContext + protected implicit val timeout: Timeout = responderData.timeout /** * Given the WHERE clause from a parsed Gravsearch query, returns information about the types found diff --git a/webapi/src/main/scala/org/knora/webapi/responders/ActorDeps.scala b/webapi/src/main/scala/org/knora/webapi/responders/ActorDeps.scala new file mode 100644 index 00000000000..dc9808c07cb --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/ActorDeps.scala @@ -0,0 +1,54 @@ +/* + * 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.actor.ActorSystem +import akka.util.Timeout +import zio.ZIO +import zio.ZLayer + +import scala.concurrent.ExecutionContext + +import org.knora.webapi.config.AppConfig +import org.knora.webapi.core.AppRouter +import org.knora.webapi.settings.KnoraDispatchers + +/** + * Class encapsulating all Akka dependencies necessary to interact with the [[org.knora.webapi.core.actors.RoutingActor]] aka. "appActor" + * + * When using this class in a service depending on the routing actor this will provide the necessary implicit dependencies for using the ask pattern + * whilst making the dependency explicit for ZIO layers. + * + * @example Usage in client code: + * {{{ + * final case class YourService(actorDeps: ActorDeps){ + * private implicit val ec: ExecutionContext = actorDeps.executionContext + * private implicit val timeout: Timeout = actorDeps.timeout + * + * private val appActor: ActorRef = actorDeps.appActor + * + * def someMethod = appActor.ask(SomeMessage())... + * } + * }}} + * + * @param system the akka.core.ActorSystem - used to extract the [[ExecutionContext]] from + * @param appActor a reference to the [[org.knora.webapi.core.actors.RoutingActor]] + * @param timeout the timeout needed for the ask pattern + */ +final case class ActorDeps(system: ActorSystem, appActor: ActorRef, timeout: Timeout) { + val executionContext: ExecutionContext = system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) +} + +object ActorDeps { + val layer: ZLayer[AppConfig with AppRouter, Nothing, ActorDeps] = ZLayer.fromZIO { + for { + router <- ZIO.service[AppRouter] + system = router.system + appActor = router.ref + timeout <- ZIO.service[AppConfig].map(_.defaultTimeoutAsDuration) + } yield ActorDeps(system, appActor, timeout) + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala b/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala new file mode 100644 index 00000000000..f2107fd9cf6 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala @@ -0,0 +1,47 @@ +package org.knora.webapi.responders +import akka.actor.Actor +import akka.actor.ActorRef +import akka.pattern.ask +import akka.util.Timeout +import zio.Tag +import zio.Task +import zio.URLayer +import zio.ZIO +import zio.ZLayer + +import scala.reflect.ClassTag + +import org.knora.webapi.messages.ResponderRequest + +/** + * This trait encapsulates the [[akka.pattern.ask]] into the zio world + */ +trait ActorToZioBridge { + + /** + * Sends a message to the "appActor" [[org.knora.webapi.core.actors.RoutingActor]] using the [[akka.pattern.ask]], + * casts and returns the response to the expected return type `R` as [[Task]]. + * + * @param message The message sent to the actor + * @param tag implicit proof that the result type `R` has a [[ClassTag]] + * + * @tparam R The type of the expected success value + * @return A Task containing either the success `R` or the failure [[Throwable]], + * will fail during runtime with a [[ClassCastException]] if the `R` does not correspond + * to the response of the message being sent due to the untyped nature of the ask pattern + */ + def askAppActor[R: Tag](message: ResponderRequest)(implicit tag: ClassTag[R]): Task[R] + +} + +final case class ActorToZioBridgeLive(actorDeps: ActorDeps) extends ActorToZioBridge { + private implicit val timeout: Timeout = actorDeps.timeout + private val appActor: ActorRef = actorDeps.appActor + + override def askAppActor[R: Tag](message: ResponderRequest)(implicit tag: ClassTag[R]): Task[R] = + ZIO.fromFuture(_ => appActor.ask(message, Actor.noSender).mapTo[R]) +} + +object ActorToZioBridge { + val live: URLayer[ActorDeps, ActorToZioBridgeLive] = ZLayer.fromFunction(ActorToZioBridgeLive.apply _) +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/EntityAndClassIriService.scala b/webapi/src/main/scala/org/knora/webapi/responders/EntityAndClassIriService.scala new file mode 100644 index 00000000000..970e0fb00d2 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/EntityAndClassIriService.scala @@ -0,0 +1,144 @@ +/* + * 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 +import akka.util.Timeout +import com.typesafe.scalalogging.LazyLogging +import zio.ZLayer + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import dsp.errors.BadRequestException +import dsp.errors.DuplicateValueException +import org.knora.webapi.IRI +import org.knora.webapi.messages.SmartIri +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.store.triplestoremessages.SparqlSelectRequest +import org.knora.webapi.messages.util.rdf.SparqlSelectResult + +/** + * This service somewhat handles checking of ontology entities and some creation of entity IRIs. + * + * It was extracted from the base class of all responders in order to be able to break up the inheritance hierarchy + * in the future - once we are porting more responders to the zio world. + * + * It is by no means complete, has already too many responsibilities and + * will be subject to further refactoring once we extract more services. + */ +final case class EntityAndClassIriService( + actorDeps: ActorDeps, + stringFormatter: StringFormatter +) extends LazyLogging { + private implicit val ec: ExecutionContext = actorDeps.executionContext + private implicit val timeout: Timeout = actorDeps.timeout + + private val appActor: ActorRef = actorDeps.appActor + + /** + * Checks whether an entity is used in the triplestore. + * + * @param entityIri the IRI of the entity. + * @param ignoreKnoraConstraints if `true`, ignores the use of the entity in Knora subject or object constraints. + * @param ignoreRdfSubjectAndObject if `true`, ignores the use of the entity in `rdf:subject` and `rdf:object`. + * + * @return `true` if the entity is used. + */ + def isEntityUsed( + entityIri: SmartIri, + ignoreKnoraConstraints: Boolean = false, + ignoreRdfSubjectAndObject: Boolean = false + ): Future[Boolean] = { + val query = org.knora.webapi.messages.twirl.queries.sparql.v2.txt + .isEntityUsed(entityIri, ignoreKnoraConstraints, ignoreRdfSubjectAndObject) + .toString() + appActor + .ask(SparqlSelectRequest(query)) + .mapTo[SparqlSelectResult] + .map(_.results.bindings.nonEmpty) + } + + /** + * Throws an exception if an entity is used in the triplestore. + * + * @param entityIri the IRI of the entity. + * @param errorFun a function that throws an exception. It will be called if the entity is used. + * @param ignoreKnoraConstraints if `true`, ignores the use of the entity in Knora subject or object constraints. + * @param ignoreRdfSubjectAndObject if `true`, ignores the use of the entity in `rdf:subject` and `rdf:object`. + */ + def throwIfEntityIsUsed( + entityIri: SmartIri, + ignoreKnoraConstraints: Boolean = false, + ignoreRdfSubjectAndObject: Boolean = false, + errorFun: => Nothing + ): Future[Unit] = + for { + entityIsUsed: Boolean <- isEntityUsed(entityIri, ignoreKnoraConstraints, ignoreRdfSubjectAndObject) + + _ = if (entityIsUsed) { + errorFun + } + } yield () + + /** + * Throws an exception if a class is used in data. + * + * @param classIri the IRI of the class. + * @param errorFun a function that throws an exception. It will be called if the class is used. + */ + def throwIfClassIsUsedInData(classIri: SmartIri, errorFun: => Nothing): Future[Unit] = + for { + classIsUsed: Boolean <- isClassUsedInData(classIri) + _ = if (classIsUsed) { errorFun } + } yield () + + /** + * Checks whether an instance of a class (or any of its sub-classes) exists. + * + * @param classIri the IRI of the class. + * @return `true` if the class is used. + */ + def isClassUsedInData(classIri: SmartIri): Future[Boolean] = { + val query = org.knora.webapi.messages.twirl.queries.sparql.v2.txt.isClassUsedInData(classIri = classIri).toString() + appActor.ask(SparqlSelectRequest(query)).mapTo[SparqlSelectResult].map(_.results.bindings.nonEmpty) + } + + /** + * Checks whether an entity with the provided custom IRI exists in the triplestore. If yes, throws an exception. + * If no custom IRI was given, creates a random unused IRI. + * + * @param entityIri the optional custom IRI of the entity. + * @param iriFormatter the stringFormatter method that must be used to create a random IRI. + * @return IRI of the entity. + */ + def checkOrCreateEntityIri(entityIri: Option[SmartIri], iriFormatter: => IRI): Future[IRI] = + entityIri match { + case Some(customEntityIri: SmartIri) => + val entityIriAsString = customEntityIri.toString + for { + + result <- stringFormatter.checkIriExists(entityIriAsString, appActor) + _ = if (result) { + throw DuplicateValueException(s"IRI: '$entityIriAsString' already exists, try another one.") + } + // Check that given entityIRI ends with a UUID + ending: String = entityIriAsString.split('/').last + _ = stringFormatter.validateBase64EncodedUuid( + ending, + throw BadRequestException(s"IRI: '$entityIriAsString' must end with a valid base 64 UUID.") + ) + + } yield entityIriAsString + + case None => stringFormatter.makeUnusedIri(iriFormatter, appActor, logger) + } +} + +object EntityAndClassIriService { + val layer: ZLayer[ActorDeps with StringFormatter, Nothing, EntityAndClassIriService] = + ZLayer.fromFunction(EntityAndClassIriService.apply _) +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala index fd534621e68..ed7652d82ae 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -9,7 +9,6 @@ package responders import akka.actor.ActorRef import akka.actor.ActorSystem import akka.http.scaladsl.util.FastFuture -import akka.pattern._ import akka.util.Timeout import com.typesafe.scalalogging.LazyLogging import com.typesafe.scalalogging.Logger @@ -18,198 +17,34 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import dsp.errors._ -import org.knora.webapi.settings.KnoraDispatchers -import org.knora.webapi.store.cache.settings.CacheServiceSettings - -import messages.store.triplestoremessages.SparqlSelectRequest -import messages.util.ResponderData -import messages.util.rdf.SparqlSelectResult -import messages.{SmartIri, StringFormatter} +import org.knora.webapi.messages.StringFormatter /** - * Responder helper methods. + * An abstract class providing values that are commonly used in responders. */ -object Responder { +abstract class Responder(actorDeps: ActorDeps) extends LazyLogging { + + protected implicit val system: ActorSystem = actorDeps.system + protected implicit val timeout: Timeout = actorDeps.timeout + protected implicit val executionContext: ExecutionContext = actorDeps.executionContext + protected implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + + protected val log: Logger = logger + protected val appActor: ActorRef = actorDeps.appActor + protected val iriService: EntityAndClassIriService = + EntityAndClassIriService(actorDeps, stringFormatter) /** - * An responder use this method to handle unexpected request messages in a consistent way. + * A responder uses this method to handle unexpected request messages in a consistent way. * * @param message the message that was received. * @param log a [[Logger]]. * @param who the responder receiving the message. */ - def handleUnexpectedMessage(message: Any, log: Logger, who: String): Future[Nothing] = { - val unexpectedMessageException = UnexpectedMessageException( - s"$who received an unexpected message $message of type ${message.getClass.getCanonicalName}" + protected def handleUnexpectedMessage(message: Any, log: Logger, who: String): Future[Nothing] = + FastFuture.failed( + UnexpectedMessageException( + s"$who received an unexpected message $message of type ${message.getClass.getCanonicalName}" + ) ) - FastFuture.failed(unexpectedMessageException) - } -} - -/** - * An abstract class providing values that are commonly used in Knora responders. - */ -abstract class Responder(responderData: ResponderData) extends LazyLogging { - - /** - * The actor system. - */ - protected implicit val system: ActorSystem = responderData.system - - /** - * The execution context for futures created in Knora actors. - */ - protected implicit val executionContext: ExecutionContext = - system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) - - /** - * The Cache Service settings. - */ - protected val cacheServiceSettings: CacheServiceSettings = responderData.cacheServiceSettings - - /** - * The main application actor. - */ - protected val appActor: ActorRef = responderData.appActor - - /** - * A string formatter. - */ - protected implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - - /** - * The application's default timeout for `ask` messages. - */ - protected implicit val timeout: Timeout = responderData.appConfig.defaultTimeoutAsDuration - - /** - * Provides logging - */ - protected val log: Logger = logger - - /** - * Checks whether an entity is used in the triplestore. - * - * @param entityIri the IRI of the entity. - * @param ignoreKnoraConstraints if `true`, ignores the use of the entity in Knora subject or object constraints. - * @param ignoreRdfSubjectAndObject if `true`, ignores the use of the entity in `rdf:subject` and `rdf:object`. - * - * @return `true` if the entity is used. - */ - protected def isEntityUsed( - entityIri: SmartIri, - ignoreKnoraConstraints: Boolean = false, - ignoreRdfSubjectAndObject: Boolean = false - ): Future[Boolean] = - for { - isEntityUsedSparql <- Future( - org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .isEntityUsed( - entityIri = entityIri, - ignoreKnoraConstraints = ignoreKnoraConstraints, - ignoreRdfSubjectAndObject = ignoreRdfSubjectAndObject - ) - .toString() - ) - - isEntityUsedResponse: SparqlSelectResult <- appActor - .ask(SparqlSelectRequest(isEntityUsedSparql)) - .mapTo[SparqlSelectResult] - - } yield isEntityUsedResponse.results.bindings.nonEmpty - - /** - * Checks whether an instance of a class (or any ob its sub-classes) exists - * - * @param classIri the IRI of the class. - * - * @return `true` if the class is used. - */ - protected def isClassUsedInData( - classIri: SmartIri - ): Future[Boolean] = - for { - isClassUsedInDataSparql <- Future( - org.knora.webapi.messages.twirl.queries.sparql.v2.txt - .isClassUsedInData( - classIri = classIri - ) - .toString() - ) - - isClassUsedInDataResponse: SparqlSelectResult <- appActor - .ask(SparqlSelectRequest(isClassUsedInDataSparql)) - .mapTo[SparqlSelectResult] - - } yield isClassUsedInDataResponse.results.bindings.nonEmpty - - /** - * Throws an exception if an entity is used in the triplestore. - * - * @param entityIri the IRI of the entity. - * @param errorFun a function that throws an exception. It will be called if the entity is used. - * @param ignoreKnoraConstraints if `true`, ignores the use of the entity in Knora subject or object constraints. - * @param ignoreRdfSubjectAndObject if `true`, ignores the use of the entity in `rdf:subject` and `rdf:object`. - */ - protected def throwIfEntityIsUsed( - entityIri: SmartIri, - errorFun: => Nothing, - ignoreKnoraConstraints: Boolean = false, - ignoreRdfSubjectAndObject: Boolean = false - ): Future[Unit] = - for { - entityIsUsed: Boolean <- isEntityUsed(entityIri, ignoreKnoraConstraints, ignoreRdfSubjectAndObject) - - _ = if (entityIsUsed) { - errorFun - } - } yield () - - /** - * Throws an exception if a class is used in data. - * - * @param classIri the IRI of the class. - * @param errorFun a function that throws an exception. It will be called if the class is used. - */ - protected def throwIfClassIsUsedInData( - classIri: SmartIri, - errorFun: => Nothing - ): Future[Unit] = - for { - classIsUsed: Boolean <- isClassUsedInData(classIri) - - _ = if (classIsUsed) { - errorFun - } - } yield () - - /** - * Checks whether an entity with the provided custom IRI exists in the triplestore, if yes, throws an exception. - * If no custom IRI was given, creates a random unused IRI. - * - * @param entityIri the optional custom IRI of the entity. - * @param iriFormatter the stringFormatter method that must be used to create a random Iri. - * @return IRI of the entity. - */ - protected def checkOrCreateEntityIri(entityIri: Option[SmartIri], iriFormatter: => IRI): Future[IRI] = - entityIri match { - case Some(customEntityIri: SmartIri) => - val entityIriAsString = customEntityIri.toString - for { - - result <- stringFormatter.checkIriExists(entityIriAsString, appActor) - _ = if (result) { - throw DuplicateValueException(s"IRI: '$entityIriAsString' already exists, try another one.") - } - // Check that given entityIRI ends with a UUID - ending: String = entityIriAsString.split('/').last - _ = stringFormatter.validateBase64EncodedUuid( - ending, - throw BadRequestException(s"IRI: '$entityIriAsString' must end with a valid base 64 UUID.") - ) - - } yield entityIriAsString - - case None => stringFormatter.makeUnusedIri(iriFormatter, appActor, log) - } } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala index 9cf5886a0d6..21274475ecd 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/GroupsResponderADM.scala @@ -28,12 +28,13 @@ import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** - * Returns information about Knora projects. + * Returns information about groups. */ -class GroupsResponderADM(responderData: ResponderData) extends Responder(responderData) with GroupsADMJsonProtocol { +class GroupsResponderADM(responderData: ResponderData) + extends Responder(responderData.actorDeps) + with GroupsADMJsonProtocol { // Global lock IRI used for group creation and updating private val GROUPS_GLOBAL_LOCK_IRI: IRI = "http://rdfh.ch/groups" @@ -451,7 +452,7 @@ class GroupsResponderADM(responderData: ResponderData) extends Responder(respond // check the custom IRI; if not given, create an unused IRI customGroupIri: Option[SmartIri] = createRequest.id.map(_.value).map(iri => iri.toSmartIri) - groupIri: IRI <- checkOrCreateEntityIri( + groupIri: IRI <- iriService.checkOrCreateEntityIri( customGroupIri, stringFormatter.makeRandomGroupIri(projectADM.shortcode) ) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala index 6ce640db108..5e93a2877fb 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala @@ -33,12 +33,11 @@ import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** - * A responder that returns information about hierarchical lists. + * A responder that returns information about lists. */ -class ListsResponderADM(responderData: ResponderData) extends Responder(responderData) { +class ListsResponderADM(responderData: ResponderData) extends Responder(responderData.actorDeps) { // The IRI used to lock user creation and update private val LISTS_GLOBAL_LOCK_IRI = "http://rdfh.ch/lists" @@ -984,7 +983,8 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde // check the custom IRI; if not given, create an unused IRI customListIri: Option[SmartIri] = id.map(_.value).map(_.toSmartIri) maybeShortcode: String = project.shortcode - newListNodeIri: IRI <- checkOrCreateEntityIri(customListIri, stringFormatter.makeRandomListIri(maybeShortcode)) + newListNodeIri: IRI <- + iriService.checkOrCreateEntityIri(customListIri, stringFormatter.makeRandomListIri(maybeShortcode)) // Create the new list node depending on type createNewListSparqlString: String = createNodeRequest match { diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponderADM.scala index 966a7b9033e..f16e432d706 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/PermissionsResponderADM.scala @@ -34,13 +34,12 @@ import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.util.cache.CacheUtil /** * Provides information about permissions to other responders. */ -class PermissionsResponderADM(responderData: ResponderData) extends Responder(responderData) { +class PermissionsResponderADM(responderData: ResponderData) extends Responder(responderData.actorDeps) { private val PERMISSIONS_GLOBAL_LOCK_IRI = "http://rdfh.ch/permissions" /* Entity types used to more clearly distinguish what kind of entity is meant */ @@ -734,11 +733,10 @@ class PermissionsResponderADM(responderData: ResponderData) extends Responder(re } customPermissionIri: Option[SmartIri] = createRequest.id.map(iri => iri.toSmartIri) - newPermissionIri: IRI <- - checkOrCreateEntityIri( - customPermissionIri, - stringFormatter.makeRandomPermissionIri(project.shortcode) - ) + newPermissionIri: IRI <- iriService.checkOrCreateEntityIri( + customPermissionIri, + stringFormatter.makeRandomPermissionIri(project.shortcode) + ) // Create the administrative permission. createAdministrativePermissionSparqlString = @@ -1650,11 +1648,10 @@ class PermissionsResponderADM(responderData: ResponderData) extends Responder(re ) customPermissionIri: Option[SmartIri] = createRequest.id.map(iri => iri.toSmartIri) - newPermissionIri: IRI <- - checkOrCreateEntityIri( - customPermissionIri, - stringFormatter.makeRandomPermissionIri(project.shortcode) - ) + newPermissionIri: IRI <- iriService.checkOrCreateEntityIri( + customPermissionIri, + stringFormatter.makeRandomPermissionIri(project.shortcode) + ) // verify group, if any given. // Is a group given that is not a built-in one? maybeGroupIri: Option[IRI] <- diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index f16e027ee35..6072d37be9b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -18,6 +18,7 @@ import scala.util.Failure import scala.util.Success import scala.util.Try +import dsp.errors.NotFoundException import dsp.errors._ import org.knora.webapi._ import org.knora.webapi.instrumentation.InstrumentationSupport @@ -36,18 +37,20 @@ import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceGetProje import org.knora.webapi.messages.store.cacheservicemessages.CacheServicePutProjectADM import org.knora.webapi.messages.store.triplestoremessages._ import org.knora.webapi.messages.util.KnoraSystemInstances -import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.rdf._ import org.knora.webapi.messages.v2.responder.ontologymessages.OntologyMetadataGetByProjectRequestV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyMetadataV2 +import org.knora.webapi.responders.ActorDeps import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage +import org.knora.webapi.store.cache.settings.CacheServiceSettings /** - * Returns information about Knora projects. + * Returns information about projects. */ -class ProjectsResponderADM(responderData: ResponderData) extends Responder(responderData) with InstrumentationSupport { +final case class ProjectsResponderADM(actorDeps: ActorDeps, cacheServiceSettings: CacheServiceSettings) + extends Responder(actorDeps) + with InstrumentationSupport { // Global lock IRI used for project creation and update private val PROJECTS_GLOBAL_LOCK_IRI = "http://rdfh.ch/projects" @@ -252,7 +255,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo * @return information about the project as a [[ProjectGetResponseADM]]. * @throws NotFoundException when no project for the given IRI can be found */ - private def getSingleProjectADMRequest( + def getSingleProjectADMRequest( identifier: ProjectIdentifierADM, requestingUser: UserADM ): Future[ProjectGetResponseADM] = @@ -1091,7 +1094,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo // check the custom IRI; if not given, create an unused IRI customProjectIri: Option[SmartIri] = createProjectRequest.id.map(_.value).map(_.toSmartIri) - newProjectIRI: IRI <- checkOrCreateEntityIri( + newProjectIRI: IRI <- iriService.checkOrCreateEntityIri( customProjectIri, stringFormatter.makeRandomProjectIri ) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala new file mode 100644 index 00000000000..448747bc97a --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala @@ -0,0 +1,34 @@ +package org.knora.webapi.responders.admin +import zio.Task +import zio.URLayer +import zio.ZLayer + +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.responders.ActorToZioBridge + +final case class ProjectsService(bridge: ActorToZioBridge) { + + /** + * Finds the project by its [[ProjectIdentifierADM]] and returns the information as a [[ProjectGetResponseADM]]. + * Checks permissions whether the [[UserADM]] requesting the project may see the result. + * + * @param identifier a [[ProjectIdentifierADM]] instance + * @param requestingUser the user making the request + * @return + * '''success''': information about the project as a [[ProjectGetResponseADM]] + * + * '''error''': [[dsp.errors.NotFoundException]] when no project for the given IRI can be found + */ + def getSingleProjectADMRequest( + identifier: ProjectIdentifierADM, + requestingUser: UserADM + ): Task[ProjectGetResponseADM] = + bridge.askAppActor(ProjectGetRequestADM(identifier, requestingUser)) +} + +object ProjectsService { + val layer: URLayer[ActorToZioBridge, ProjectsService] = ZLayer.fromFunction(ProjectsService.apply _) +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala index 753667aaa73..86168f7d334 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala @@ -29,13 +29,12 @@ import org.knora.webapi.messages.util.PermissionUtilADM import org.knora.webapi.messages.util.PermissionUtilADM.EntityPermission import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Responds to requests for information about binary representations of resources, and returns responses in Knora API * ADM format. */ -class SipiResponderADM(responderData: ResponderData) extends Responder(responderData) { +class SipiResponderADM(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * Receives a message of type [[SipiResponderRequestADM]], and returns an appropriate response message, or diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/StoresResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/StoresResponderADM.scala index d657292e917..10ebd72ebd1 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/StoresResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/StoresResponderADM.scala @@ -23,13 +23,12 @@ import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages.LoadOntologiesRequestV2 import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * This responder is used by [[org.knora.webapi.routing.admin.StoreRouteADM]], for piping through HTTP requests to the * 'Store Module' */ -class StoresResponderADM(responderData: ResponderData) extends Responder(responderData) { +class StoresResponderADM(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * A user representing the Knora API server, used in those cases where a user is required. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala index f36acedff1d..a67d98f5456 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala @@ -39,12 +39,13 @@ import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Provides information about Knora users to other responders. */ -class UsersResponderADM(responderData: ResponderData) extends Responder(responderData) with InstrumentationSupport { +class UsersResponderADM(responderData: ResponderData) + extends Responder(responderData.actorDeps) + with InstrumentationSupport { // The IRI used to lock user creation and update private val USERS_GLOBAL_LOCK_IRI = "http://rdfh.ch/users" @@ -1707,7 +1708,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde // check the custom IRI; if not given, create an unused IRI customUserIri: Option[SmartIri] = userCreatePayloadADM.id.map(_.value.toSmartIri) - userIri: IRI <- checkOrCreateEntityIri(customUserIri, stringFormatter.makeRandomPersonIri) + userIri: IRI <- iriService.checkOrCreateEntityIri(customUserIri, stringFormatter.makeRandomPersonIri) // hash password encoder = new BCryptPasswordEncoder(responderData.appConfig.bcryptPasswordStrength) @@ -1804,7 +1805,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde private def getUserFromCacheOrTriplestore( identifier: UserIdentifierADM ): Future[Option[UserADM]] = tracedFuture("admin-user-get-user-from-cache-or-triplestore") { - if (cacheServiceSettings.cacheServiceEnabled) { + if (responderData.cacheServiceSettings.cacheServiceEnabled) { // caching enabled getUserFromCache(identifier).flatMap { case None => @@ -2194,7 +2195,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde * @return a [[Unit]] */ private def invalidateCachedUserADM(maybeUser: Option[UserADM]): Future[Unit] = - if (cacheServiceSettings.cacheServiceEnabled) { + if (responderData.cacheServiceSettings.cacheServiceEnabled) { val keys: Set[String] = Seq(maybeUser.map(_.id), maybeUser.map(_.email), maybeUser.map(_.username)).flatten.toSet // only send to Redis if keys are not empty if (keys.nonEmpty) { diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/CkanResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/CkanResponderV1.scala index 8bfcd67de46..6d455a23ed9 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/CkanResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/CkanResponderV1.scala @@ -34,13 +34,12 @@ import org.knora.webapi.messages.v1.responder.valuemessages.HierarchicalListValu import org.knora.webapi.messages.v1.responder.valuemessages.LinkV1 import org.knora.webapi.messages.v1.responder.valuemessages.TextValueV1 import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * This responder is used by the Ckan route, for serving data to the Ckan harverster, which is published * under http://data.humanities.ch */ -class CkanResponderV1(responderData: ResponderData) extends Responder(responderData) { +class CkanResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * A user representing the Knora API server, used in those cases where a user is required. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/ListsResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/ListsResponderV1.scala index 0d5f0c95988..a2318a50728 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/ListsResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/ListsResponderV1.scala @@ -19,12 +19,11 @@ import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.messages.v1.responder.listmessages._ import org.knora.webapi.messages.v1.responder.usermessages.UserProfileV1 import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * A responder that returns information about hierarchical lists. */ -class ListsResponderV1(responderData: ResponderData) extends Responder(responderData) { +class ListsResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * Receives a message of type [[ListsResponderRequestV1]], and returns an appropriate response message. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/OntologyResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/OntologyResponderV1.scala index 5bd5c6be93d..b3efa7b2c0a 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/OntologyResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/OntologyResponderV1.scala @@ -27,7 +27,6 @@ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.KnoraCardinalityInfo import org.knora.webapi.messages.v2.responder.ontologymessages._ import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Handles requests for information about ontology entities. @@ -35,7 +34,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage * All ontology data is loaded and cached when the application starts. To refresh the cache, you currently have to restart * the application. */ -class OntologyResponderV1(responderData: ResponderData) extends Responder(responderData) { +class OntologyResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { private val valueUtilV1 = new ValueUtilV1(responderData.appConfig) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/ProjectsResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/ProjectsResponderV1.scala index e4a1650f1fc..1493e3e733b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/ProjectsResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/ProjectsResponderV1.scala @@ -20,7 +20,6 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserIdentifierADM import org.knora.webapi.messages.admin.responder.usersmessages.UserResponseADM import org.knora.webapi.messages.store.triplestoremessages._ import org.knora.webapi.messages.util.KnoraSystemInstances -import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.messages.v1.responder.ontologymessages.NamedGraphV1 @@ -28,13 +27,13 @@ import org.knora.webapi.messages.v1.responder.ontologymessages.NamedGraphsGetReq import org.knora.webapi.messages.v1.responder.ontologymessages.NamedGraphsResponseV1 import org.knora.webapi.messages.v1.responder.projectmessages._ import org.knora.webapi.messages.v1.responder.usermessages._ +import org.knora.webapi.responders.ActorDeps import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Returns information about Knora projects. */ -class ProjectsResponderV1(responderData: ResponderData) extends Responder(responderData) { +final case class ProjectsResponderV1(actorDeps: ActorDeps) extends Responder(actorDeps) { // Global lock IRI used for project creation and update diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala index e8ee008c176..e553f110f75 100755 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala @@ -50,7 +50,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyMetad import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2 import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.responders.v2.ResourceUtilV2 import org.knora.webapi.util.ActorUtil import org.knora.webapi.util.ApacheLuceneSupport.MatchStringWhileTyping @@ -58,7 +57,7 @@ import org.knora.webapi.util.ApacheLuceneSupport.MatchStringWhileTyping /** * Responds to requests for information about resources, and returns responses in Knora API v1 format. */ -class ResourcesResponderV1(responderData: ResponderData) extends Responder(responderData) { +class ResourcesResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { // Converts SPARQL query results to ApiValueV1 objects. private val valueUtilV1 = new ValueUtilV1(responderData.appConfig) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/SearchResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/SearchResponderV1.scala index 1bd3607d4f9..e6677d31e8d 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/SearchResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/SearchResponderV1.scala @@ -28,14 +28,13 @@ import org.knora.webapi.messages.v1.responder.ontologymessages._ import org.knora.webapi.messages.v1.responder.searchmessages._ import org.knora.webapi.messages.v1.responder.valuemessages.KnoraCalendarV1 import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.util.ApacheLuceneSupport.LuceneQueryString /** * Responds to requests for user search queries and returns responses in Knora API * v1 format. */ -class SearchResponderV1(responderData: ResponderData) extends Responder(responderData) { +class SearchResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { // Valid combinations of value types and comparison operators, for determining whether a requested search // criterion is valid. The valid comparison operators for search criteria involving link properties can be diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala index d73a7eb54ed..e2316484998 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/StandoffResponderV1.scala @@ -21,13 +21,12 @@ import org.knora.webapi.messages.v1.responder.ontologymessages.StandoffEntityInf import org.knora.webapi.messages.v1.responder.standoffmessages._ import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.store.iiif.errors.SipiException /** * Responds to requests relating to the creation of mappings from XML elements and attributes to standoff classes and properties. */ -class StandoffResponderV1(responderData: ResponderData) extends Responder(responderData) { +class StandoffResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * Receives a message of type [[StandoffResponderRequestV1]], and returns an appropriate response message. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/UsersResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/UsersResponderV1.scala index 32f71ed5d71..221bcd3bee8 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/UsersResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/UsersResponderV1.scala @@ -28,13 +28,12 @@ import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1 import org.knora.webapi.messages.v1.responder.usermessages.UserProfileTypeV1.UserProfileType import org.knora.webapi.messages.v1.responder.usermessages._ import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.util.cache.CacheUtil /** * Provides information about Knora users to other responders. */ -class UsersResponderV1(responderData: ResponderData) extends Responder(responderData) { +class UsersResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { // The IRI used to lock user creation and update val USERS_GLOBAL_LOCK_IRI = "http://rdfh.ch/users" diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala index a9c1115fbe3..1e218cb8dfd 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala @@ -47,14 +47,13 @@ import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2 import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.responders.v2.ResourceUtilV2 import org.knora.webapi.util._ /** * Updates Knora values. */ -class ValuesResponderV1(responderData: ResponderData) extends Responder(responderData) { +class ValuesResponderV1(responderData: ResponderData) extends Responder(responderData.actorDeps) { // Converts SPARQL query results to ApiValueV1 objects. val valueUtilV1 = new ValueUtilV1(responderData.appConfig) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala index 217e685d8e0..ac336ec4b11 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ListsResponderV2.scala @@ -18,12 +18,11 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.v2.responder.listsmessages._ import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Responds to requests relating to lists and nodes. */ -class ListsResponderV2(responderData: ResponderData) extends Responder(responderData) { +class ListsResponderV2(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * Receives a message of type [[ListsResponderRequestV2]], and returns an appropriate response message inside a future. 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 6e9c03719f9..a523c60ea3d 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 @@ -34,7 +34,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.Kn import org.knora.webapi.messages.v2.responder.ontologymessages._ import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.responders.v2.ontology.Cache import org.knora.webapi.responders.v2.ontology.Cache.ONTOLOGY_CACHE_LOCK_IRI import org.knora.webapi.responders.v2.ontology.CardinalityHandler @@ -60,7 +59,7 @@ import org.knora.webapi.util._ * * The API v1 ontology responder, which is read-only, delegates most of its work to this responder. */ -class OntologyResponderV2(responderData: ResponderData) extends Responder(responderData) { +class OntologyResponderV2(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * Receives a message of type [[OntologiesResponderRequestV2]], and returns an appropriate response message. @@ -1312,7 +1311,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon _ <- hasCardinality match { // If there is, check that the class isn't used in data. case Some((propIri: SmartIri, cardinality: KnoraCardinalityInfo)) => - throwIfClassIsUsedInData( + iriService.throwIfClassIsUsedInData( classIri = internalClassIri, errorFun = throw BadRequestException( s"Cardinality ${cardinality.toString} for $propIri cannot be added to class ${addCardinalitiesRequest.classInfoContent.classIri}, because it is used in data" @@ -1507,7 +1506,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon userCanUpdateOntology <- OntologyHelpers.canUserUpdateOntology(internalOntologyIri, canChangeCardinalitiesRequest.requestingUser) - classIsUsed <- isEntityUsed( + 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. @@ -1563,13 +1562,12 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the class isn't used in data, and that it has no subclasses. // TODO: If class is used in data, check additionally if the property(ies) being removed is(are) truly used and if not, then allow. - _ <- throwIfEntityIsUsed( + _ <- iriService.throwIfEntityIsUsed( entityIri = internalClassIri, + ignoreKnoraConstraints = true, errorFun = throw BadRequestException( s"The cardinalities of class ${changeCardinalitiesRequest.classInfoContent.classIri} cannot be changed, because it is used in data or has a subclass" - ), - ignoreKnoraConstraints = - true // It's OK if a property refers to the class via knora-base:subjectClassConstraint or knora-base:objectClassConstraint. + ) ) // Make an updated class definition. @@ -1827,7 +1825,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon userCanUpdateOntology <- OntologyHelpers.canUserUpdateOntology(internalOntologyIri, canDeleteClassRequest.requestingUser) - classIsUsed <- isEntityUsed(entityIri = internalClassIri) + classIsUsed <- iriService.isEntityUsed(entityIri = internalClassIri) } yield CanDoResponseV2(userCanUpdateOntology && !classIsUsed) } @@ -1859,7 +1857,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the class isn't used in data or ontologies. - _ <- throwIfEntityIsUsed( + _ <- iriService.throwIfEntityIsUsed( entityIri = internalClassIri, errorFun = throw BadRequestException( s"Class ${deleteClassRequest.classIri} cannot be deleted, because it is used in data or ontologies" @@ -1965,7 +1963,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon userCanUpdateOntology <- OntologyHelpers.canUserUpdateOntology(internalOntologyIri, canDeletePropertyRequest.requestingUser) - propertyIsUsed <- isEntityUsed(internalPropertyIri) + propertyIsUsed <- iriService.isEntityUsed(internalPropertyIri) } yield CanDoResponseV2(userCanUpdateOntology && !propertyIsUsed) } @@ -2011,7 +2009,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the property isn't used in data or ontologies. - _ <- throwIfEntityIsUsed( + _ <- iriService.throwIfEntityIsUsed( entityIri = internalPropertyIri, errorFun = throw BadRequestException( s"Property ${deletePropertyRequest.propertyIri} cannot be deleted, because it is used in data or ontologies" @@ -2020,7 +2018,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon _ <- maybeInternalLinkValuePropertyIri match { case Some(internalLinkValuePropertyIri) => - throwIfEntityIsUsed( + iriService.throwIfEntityIsUsed( entityIri = internalLinkValuePropertyIri, errorFun = throw BadRequestException( s"Property ${deletePropertyRequest.propertyIri} cannot be deleted, because the corresponding link value property, ${internalLinkValuePropertyIri diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index c8e2fff2890..3b527b0ae0a 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -60,7 +60,6 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.GetXSLTransformat import org.knora.webapi.messages.v2.responder.standoffmessages.GetXSLTransformationResponseV2 import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.responders.IriLocker -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util._ @@ -360,7 +359,7 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt } resourceIri: IRI <- - checkOrCreateEntityIri( + iriService.checkOrCreateEntityIri( createResourceRequestV2.createResource.resourceIri, stringFormatter.makeRandomResourceIri(createResourceRequestV2.createResource.projectADM.shortcode) ) @@ -705,12 +704,12 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt resourceSmartIri = eraseResourceV2.resourceIri.toSmartIri - _ <- throwIfEntityIsUsed( + _ <- iriService.throwIfEntityIsUsed( entityIri = resourceSmartIri, + ignoreRdfSubjectAndObject = true, errorFun = throw BadRequestException( s"Resource ${eraseResourceV2.resourceIri} cannot be erased, because it is referred to by another resource" - ), - ignoreRdfSubjectAndObject = true + ) ) // Get the IRI of the named graph from which the resource will be erased. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala index 670ae124f23..559dea6c67d 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResponderWithStandoffV2.scala @@ -27,7 +27,7 @@ import org.knora.webapi.store.iiif.errors.SipiException /** * An abstract class with standoff utility methods for v2 responders. */ -abstract class ResponderWithStandoffV2(responderData: ResponderData) extends Responder(responderData) { +abstract class ResponderWithStandoffV2(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * Gets mappings referred to in query results [[Map[IRI, ResourceWithValueRdfData]]]. diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index 7c500d0b7d5..766d640125b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -46,7 +46,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV2 import org.knora.webapi.messages.v2.responder.resourcemessages._ import org.knora.webapi.messages.v2.responder.searchmessages._ -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException import org.knora.webapi.util.ApacheLuceneSupport._ diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala index c328ecb3f3f..20ab27bcc8f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/StandoffResponderV2.scala @@ -54,14 +54,13 @@ import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.util._ import org.knora.webapi.util.cache.CacheUtil /** * Responds to requests relating to the creation of mappings from XML elements and attributes to standoff classes and properties. */ -class StandoffResponderV2(responderData: ResponderData) extends Responder(responderData) { +class StandoffResponderV2(responderData: ResponderData) extends Responder(responderData.actorDeps) { private def xmlMimeTypes = Set( "text/xml", diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 5b0c1848411..64399e7c896 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -37,13 +37,12 @@ import org.knora.webapi.messages.v2.responder.searchmessages.GravsearchRequestV2 import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.Responder -import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.util.ActorUtil /** * Handles requests to read and write Knora values. */ -class ValuesResponderV2(responderData: ResponderData) extends Responder(responderData) { +class ValuesResponderV2(responderData: ResponderData) extends Responder(responderData.actorDeps) { /** * The IRI and content of a new value or value version whose existence in the triplestore has been verified. @@ -481,7 +480,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde newValueUUID: UUID <- Future.successful(makeNewValueUUID(maybeValueIri, maybeValueUUID)) // Make an IRI for the new value. - newValueIri: IRI <- checkOrCreateEntityIri( + newValueIri: IRI <- iriService.checkOrCreateEntityIri( maybeValueIri, stringFormatter.makeRandomValueIri(resourceInfo.resourceIri, Some(newValueUUID)) ) @@ -709,7 +708,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde makeNewValueUUID(valueToCreate.customValueIri, valueToCreate.customValueUUID) ) - newValueIri: IRI <- checkOrCreateEntityIri( + newValueIri: IRI <- iriService.checkOrCreateEntityIri( valueToCreate.customValueIri, stringFormatter.makeRandomValueIri(resourceIri, Some(newValueUUID)) ) @@ -1066,7 +1065,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde // Do the update. dataNamedGraph: IRI = stringFormatter.projectDataNamedGraphV2(resourceInfo.projectADM) - newValueIri: IRI <- checkOrCreateEntityIri( + newValueIri: IRI <- iriService.checkOrCreateEntityIri( updateValuePermissionsV2.newValueVersionIri, stringFormatter.makeRandomValueIri(resourceInfo.resourceIri) ) @@ -1388,7 +1387,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde requestingUser: UserADM ): Future[UnverifiedValueV2] = for { - newValueIri: IRI <- checkOrCreateEntityIri( + newValueIri: IRI <- iriService.checkOrCreateEntityIri( newValueVersionIri, stringFormatter.makeRandomValueIri(resourceInfo.resourceIri) ) @@ -2444,7 +2443,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde for { // Make an IRI for the new LinkValue. - newLinkValueIri: IRI <- checkOrCreateEntityIri( + newLinkValueIri: IRI <- iriService.checkOrCreateEntityIri( customNewLinkValueIri, stringFormatter.makeRandomValueIri(sourceResourceInfo.resourceIri) ) @@ -2600,7 +2599,7 @@ class ValuesResponderV2(responderData: ResponderData) extends Responder(responde for { // If no custom IRI was provided, generate an IRI for the new LinkValue. - newLinkValueIri: IRI <- checkOrCreateEntityIri( + newLinkValueIri: IRI <- iriService.checkOrCreateEntityIri( customNewLinkValueIri, stringFormatter.makeRandomValueIri(sourceResourceInfo.resourceIri) ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala index 69b1ddc3bb9..b3fc4314c34 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala @@ -1,9 +1,12 @@ +/* + * 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 -import akka.pattern.ask -import akka.util.Timeout import zhttp.http._ +import zio.URLayer import zio.ZIO import zio.ZLayer @@ -14,16 +17,12 @@ import dsp.errors.InternalServerException import dsp.errors.KnoraException import dsp.errors.RequestRejectedException import org.knora.webapi.config.AppConfig -import org.knora.webapi.core.AppRouter import org.knora.webapi.http.handler.ExceptionHandlerZ -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.messages.util.KnoraSystemInstances +import org.knora.webapi.responders.admin.ProjectsService -final case class ProjectsRouteZ(router: AppRouter, appConfig: AppConfig) { - implicit val sender: ActorRef = router.ref - implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration +final case class ProjectsRouteZ(projectsService: ProjectsService, appConfig: AppConfig) { def getProjectByIri(iri: String): ZIO[Any, KnoraException, Response] = for { @@ -32,11 +31,8 @@ final case class ProjectsRouteZ(router: AppRouter, appConfig: AppConfig) { ZIO .attempt(URLDecoder.decode(iri, "utf-8")) .orElseFail(BadRequestException(s"Failed to decode IRI $iri")) - iriValue <- ProjectIdentifierADM.IriIdentifier - .fromString(iriDecoded) - .toZIO - message = ProjectGetRequestADM(identifier = iriValue, requestingUser = user) - response <- ZIO.fromFuture(_ => router.ref.ask(message)).map(_.asInstanceOf[ProjectGetResponseADM]).orDie + iriValue <- ProjectIdentifierADM.IriIdentifier.fromString(iriDecoded).toZIO + response <- projectsService.getSingleProjectADMRequest(iriValue, user).orDie } yield Response.json(response.toJsValue.toString()) val route: HttpApp[Any, Nothing] = @@ -56,7 +52,5 @@ final case class ProjectsRouteZ(router: AppRouter, appConfig: AppConfig) { } object ProjectsRouteZ { - val layer: ZLayer[AppRouter with AppConfig, Nothing, ProjectsRouteZ] = ZLayer.fromFunction { (router, config) => - ProjectsRouteZ(router, config) - } + val layer: URLayer[ProjectsService with AppConfig, ProjectsRouteZ] = ZLayer.fromFunction(ProjectsRouteZ.apply _) } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/ActorToZioBridgeMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/ActorToZioBridgeMock.scala new file mode 100644 index 00000000000..5a249d0f607 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/responders/ActorToZioBridgeMock.scala @@ -0,0 +1,33 @@ +package org.knora.webapi.responders +import zio.Tag +import zio.Task +import zio.URLayer +import zio.ZIO +import zio.ZLayer +import zio.mock +import zio.mock.Mock +import zio.mock.Proxy + +import scala.reflect.ClassTag + +import org.knora.webapi.messages.ResponderRequest + +/** + * zio-mock implementation for the [[ActorToZioBridge]] + * + * See also the zio-mock documentation: + * [[https://zio.dev/ecosystem/officials/zio-mock/#encoding-polymorphic-capabilities]] + */ +object ActorToZioBridgeMock extends Mock[ActorToZioBridge] { + object AskAppActor extends Poly.Effect.Output[ResponderRequest, Throwable] + + val compose: URLayer[mock.Proxy, ActorToZioBridge] = + ZLayer { + for { + proxy <- ZIO.service[Proxy] + } yield new ActorToZioBridge { + override def askAppActor[R: Tag](message: ResponderRequest)(implicit tag: ClassTag[R]): Task[R] = + proxy(AskAppActor.of[R], message) + } + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceSpec.scala new file mode 100644 index 00000000000..b9669ed68fc --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceSpec.scala @@ -0,0 +1,54 @@ +package org.knora.webapi.responders.admin +import zio.Scope +import zio.ZIO +import zio.mock._ +import zio.test.Assertion +import zio.test.Spec +import zio.test.TestEnvironment +import zio.test.ZIOSpecDefault +import zio.test.assertTrue + +import dsp.valueobjects.Project.ShortCode +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.messages.util.KnoraSystemInstances +import org.knora.webapi.responders.ActorToZioBridgeMock + +object ProjectsServiceSpec extends ZIOSpecDefault { + + private val id: ShortcodeIdentifier = ShortcodeIdentifier( + ShortCode.make("0001").getOrElse(throw new IllegalArgumentException()) + ) + private val user: UserADM = KnoraSystemInstances.Users.SystemUser + private val expectedRequest: ProjectGetRequestADM = ProjectGetRequestADM(id, user) + private val expectedResponse: ProjectGetResponseADM = ProjectGetResponseADM( + ProjectADM( + "id", + "shortname", + "shortcode", + None, + List(StringLiteralV2("description")), + List.empty, + None, + List.empty, + status = false, + selfjoin = false + ) + ) + + private val expectation = ActorToZioBridgeMock.AskAppActor + .of[ProjectGetResponseADM] + .apply(Assertion.equalTo(expectedRequest), Expectation.value(expectedResponse)) + + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("ProjectsService")(test("should send correct message and return expected response") { + for { + sut <- ZIO.service[ProjectsService] + actual <- sut.getSingleProjectADMRequest(id, user) + } yield assertTrue(actual == expectedResponse) + }).provide(ProjectsService.layer, expectation) +}