From 9c20c33afb60e715a60a21bb3bfbfb279b7ef01b Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 11:51:13 +0200 Subject: [PATCH 01/44] add placeholders to not invoke warnings --- .../dsp/project/listener/external/ProjectListenerExternal.scala | 2 ++ .../dsp/project/listener/internal/ProjectListenerInternal.scala | 2 ++ .../src/main/scala/dsp/project/route/ProjectRoute.scala | 2 ++ .../project/listener/external/ProjectListenerExternalSpec.scala | 2 ++ .../project/listener/internal/ProjectListenerInternalSpec.scala | 2 ++ .../src/test/scala/dsp/project/route/ProjectRouteSpec.scala | 2 ++ 6 files changed, 12 insertions(+) diff --git a/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala b/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala index 24dd0971ac..8d3e981446 100644 --- a/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala +++ b/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala @@ -4,3 +4,5 @@ */ package dsp.project.listener.external + +object placeholder {} diff --git a/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala b/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala index d1250b9f2a..20a476fe06 100644 --- a/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala +++ b/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala @@ -4,3 +4,5 @@ */ package dsp.project.listener.internal + +object placeholder {} diff --git a/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala b/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala index 13b9eaef43..51f5cd5171 100644 --- a/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala +++ b/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala @@ -4,3 +4,5 @@ */ package dsp.project.route + +object placeholder {} diff --git a/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala b/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala index 24dd0971ac..8d3e981446 100644 --- a/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala +++ b/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala @@ -4,3 +4,5 @@ */ package dsp.project.listener.external + +object placeholder {} diff --git a/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala b/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala index d1250b9f2a..20a476fe06 100644 --- a/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala +++ b/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala @@ -4,3 +4,5 @@ */ package dsp.project.listener.internal + +object placeholder {} diff --git a/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala b/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala index 13b9eaef43..51f5cd5171 100644 --- a/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala +++ b/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala @@ -4,3 +4,5 @@ */ package dsp.project.route + +object placeholder {} From a96324676128e62a9645595ade8917e3e175838d Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 13:38:52 +0200 Subject: [PATCH 02/44] remove settings from OntologyHelpers --- webapi/src/main/resources/application.conf | 12 ++-- .../org/knora/webapi/config/AppConfig.scala | 48 ++++++++++++++-- .../webapi/core/actors/RoutingActor.scala | 2 +- .../responders/v2/OntologyResponderV2.scala | 55 ++----------------- .../v2/ontology/CardinalityHandler.scala | 4 -- .../v2/ontology/OntologyHelpers.scala | 18 ++---- .../knora/webapi/settings/KnoraSettings.scala | 10 ++-- .../store/iiif/impl/IIIFServiceSipiImpl.scala | 4 +- 8 files changed, 65 insertions(+), 88 deletions(-) diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index 5d1b12772c..c89f5d98de 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -304,11 +304,9 @@ app { file-server-path = "server" - v2 { - file-metadata-route = "knora.json" - move-file-route = "store" - delete-temp-file-route = "delete_temp_file" - } + file-metadata-route = "knora.json" + move-file-route = "store" + delete-temp-file-route = "delete_temp_file" image-mime-types = ["image/tiff", "image/jpeg", "image/png", "image/jp2", "image/jpx"] document-mime-types = [ @@ -430,8 +428,8 @@ app { gui { // The default size of resource type icons. TODO: put icon sizes in the triplestore instead. default-icon-size { - dimX = 32 - dimY = 32 + dim-x = 32 + dim-y = 32 } } diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 4e606654e8..cff4e65c2a 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -30,10 +30,16 @@ final case class AppConfig( knoraApi: KnoraAPI, sipi: Sipi, ark: Ark, + salsah1: Salsah1, + gui: Gui, triplestore: Triplestore, - shacl: Shacl + v2: V2, + shacl: Shacl, + user: User ) { val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity) + val defaultTimeoutAsDuration = + scala.concurrent.duration.Duration.apply(defaultTimeout).asInstanceOf[duration.FiniteDuration] } final case class KnoraAPI( @@ -79,7 +85,8 @@ final case class Sipi( externalHost: String, externalPort: Int, fileServerPath: String, - v2: V2, + moveFileRoute: String, + deleteTempFileRoute: String, imageMimeTypes: List[String], documentMimeTypes: List[String], textMimeTypes: List[String], @@ -94,12 +101,40 @@ final case class Sipi( ":" + externalPort else "") val timeoutInSeconds: duration.Duration = scala.concurrent.duration.Duration(timeout) + } final case class V2( - fileMetadataRoute: String, - moveFileRoute: String, - deleteTempFileRoute: String + resourcesSequence: ResourcesSequence, + graphRoute: GraphRoute, + fulltextSearch: FulltextSearch +) + +final case class ResourcesSequence( + resultsPerPage: Int +) + +final case class GraphRoute( + defaultGraphDepth: Int, + maxGraphDepth: Int +) + +final case class FulltextSearch( + searchValueMinLength: Int +) + +final case class Salsah1( + baseUrl: String, + projectIconsBasepath: String +) + +final case class Gui( + defaultIconSize: DefaultIconSize +) + +final case class DefaultIconSize( + dimX: Int, + dimY: Int ) final case class Ark( @@ -141,6 +176,9 @@ final case class Shacl( ) { val shapesDirPath = Paths.get(shapesDir) } +final case class User( + defaultLanguage: String = "en" +) /** * Loads the applicaton configuration using ZIO-Config. ZIO-Config is capable of loading 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 40998b6f49..3824fe361a 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 @@ -120,7 +120,7 @@ class RoutingActor( val projectsResponderV1: ProjectsResponderV1 = new ProjectsResponderV1(responderData) // V2 responders - val ontologiesResponderV2: OntologyResponderV2 = new OntologyResponderV2(responderData) + val ontologiesResponderV2: OntologyResponderV2 = new OntologyResponderV2(responderData, appConfig) val searchResponderV2: SearchResponderV2 = new SearchResponderV2(responderData) val resourcesResponderV2: ResourcesResponderV2 = new ResourcesResponderV2(responderData) val valuesResponderV2: ValuesResponderV2 = new ValuesResponderV2(responderData) 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 c99e02bef2..2d66e2b7c4 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 @@ -40,6 +40,7 @@ import org.knora.webapi.responders.v2.ontology.Cache.ONTOLOGY_CACHE_LOCK_IRI import org.knora.webapi.responders.v2.ontology.CardinalityHandler import org.knora.webapi.responders.v2.ontology.OntologyHelpers import org.knora.webapi.util._ +import org.knora.webapi.config.AppConfig /** * Responds to requests dealing with ontologies. @@ -60,7 +61,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, config: AppConfig) extends Responder(responderData) { /** * Receives a message of type [[OntologiesResponderRequestV2]], and returns an appropriate response message. @@ -293,7 +294,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon label = classInfo.entityInfoContent .getPredicateStringLiteralObject( predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, - preferredLangs = Some(requestingUser.lang, settings.fallbackLanguage) + preferredLangs = Some(requestingUser.lang, config.user.defaultLanguage) ) .getOrElse( throw InconsistentRepositoryDataException(s"Resource class $subClassIri has no rdfs:label") @@ -508,7 +509,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Make sure the ontology doesn't already exist. existingOntologyMetadata: Option[OntologyMetadataV2] <- OntologyHelpers.loadOntologyMetadata( - settings, appActor, internalOntologyIri = internalOntologyIri ) @@ -568,7 +568,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon ).unescape maybeLoadedOntologyMetadata: Option[OntologyMetadataV2] <- OntologyHelpers.loadOntologyMetadata( - settings, appActor, internalOntologyIri = internalOntologyIri ) @@ -662,7 +661,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read its metadata. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = changeOntologyMetadataRequest.lastModificationDate @@ -724,7 +722,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon maybeLoadedOntologyMetadata: Option[OntologyMetadataV2] <- OntologyHelpers.loadOntologyMetadata( - settings, appActor, internalOntologyIri = internalOntologyIri ) @@ -777,7 +774,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read its metadata. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deleteOntologyCommentRequestV2.lastModificationDate @@ -819,7 +815,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon maybeLoadedOntologyMetadata: Option[OntologyMetadataV2] <- OntologyHelpers.loadOntologyMetadata( - settings, appActor, internalOntologyIri = internalOntologyIri ) @@ -869,7 +864,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = createClassRequest.lastModificationDate @@ -1004,7 +998,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -1013,7 +1006,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the data that was saved corresponds to the data that was submitted. loadedClassDef <- OntologyHelpers.loadClassDefinition( - settings, appActor, classIri = internalClassIri ) @@ -1087,7 +1079,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = changeGuiOrderRequest.lastModificationDate @@ -1181,7 +1172,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -1190,7 +1180,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the data that was saved corresponds to the data that was submitted. loadedClassDef: ClassInfoContentV2 <- OntologyHelpers.loadClassDefinition( - settings, appActor, classIri = internalClassIri ) @@ -1269,7 +1258,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = addCardinalitiesRequest.lastModificationDate @@ -1424,7 +1412,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -1433,7 +1420,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the data that was saved corresponds to the data that was submitted. loadedClassDef <- OntologyHelpers.loadClassDefinition( - settings, appActor, classIri = internalClassIri ) @@ -1547,7 +1533,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = changeCardinalitiesRequest.lastModificationDate @@ -1673,7 +1658,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -1682,7 +1666,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the data that was saved corresponds to the data that was submitted. loadedClassDef <- OntologyHelpers.loadClassDefinition( - settings, appActor, classIri = internalClassIri ) @@ -1864,7 +1847,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deleteClassRequest.lastModificationDate @@ -1906,7 +1888,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -2004,7 +1985,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deletePropertyRequest.lastModificationDate @@ -2074,7 +2054,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -2145,7 +2124,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon userCanUpdateOntology <- OntologyHelpers.canUserUpdateOntology(internalOntologyIri, canDeleteOntologyRequest.requestingUser) - subjectsUsingOntology <- OntologyHelpers.getSubjectsUsingOntology(settings, appActor, ontology) + subjectsUsingOntology <- OntologyHelpers.getSubjectsUsingOntology(appActor, ontology) } yield CanDoResponseV2(userCanUpdateOntology && subjectsUsingOntology.isEmpty) } @@ -2162,7 +2141,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deleteOntologyRequest.lastModificationDate @@ -2171,7 +2149,7 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that none of the entities in the ontology are used in data or in other ontologies. ontology = cacheData.ontologies(internalOntologyIri) - subjectsUsingOntology: Set[IRI] <- OntologyHelpers.getSubjectsUsingOntology(settings, appActor, ontology) + subjectsUsingOntology: Set[IRI] <- OntologyHelpers.getSubjectsUsingOntology(appActor, ontology) _ = if (subjectsUsingOntology.nonEmpty) { val sortedSubjects: Seq[IRI] = subjectsUsingOntology.map(s => "<" + s + ">").toVector.sorted @@ -2195,7 +2173,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology has been deleted. maybeOntologyMetadata <- OntologyHelpers.loadOntologyMetadata( - settings, appActor, internalOntologyIri = internalOntologyIri ) @@ -2240,7 +2217,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = createPropertyRequest.lastModificationDate @@ -2459,7 +2435,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -2469,7 +2444,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // we have to undo the SPARQL-escaping of the input. loadedPropertyDef <- OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = internalPropertyIri ) @@ -2485,7 +2459,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon maybeLoadedLinkValuePropertyDefFuture: Option[Future[PropertyInfoContentV2]] = maybeLinkValuePropertyDef.map { linkValuePropertyDef => OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = linkValuePropertyDef.propertyIri ) @@ -2598,7 +2571,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = changePropertyGuiElementRequest.lastModificationDate @@ -2651,7 +2623,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -2661,7 +2632,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // we have to undo the SPARQL-escaping of the input. loadedPropertyDef <- OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = internalPropertyIri ) @@ -2704,7 +2674,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon maybeLoadedLinkValuePropertyDefFuture: Option[Future[PropertyInfoContentV2]] = maybeCurrentLinkValueReadPropertyInfo.map { linkValueReadPropertyInfo => OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = linkValueReadPropertyInfo.entityInfoContent.propertyIri ) @@ -2825,7 +2794,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = changePropertyLabelsOrCommentsRequest.lastModificationDate @@ -2872,7 +2840,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings = settings, appActor = appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -2882,7 +2849,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // we have to undo the SPARQL-escaping of the input. loadedPropertyDef <- OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = internalPropertyIri ) @@ -2908,7 +2874,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon maybeLoadedLinkValuePropertyDefFuture: Option[Future[PropertyInfoContentV2]] = maybeCurrentLinkValueReadPropertyInfo.map { linkValueReadPropertyInfo => OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = linkValueReadPropertyInfo.entityInfoContent.propertyIri ) @@ -3025,7 +2990,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = changeClassLabelsOrCommentsRequest.lastModificationDate @@ -3052,7 +3016,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -3062,7 +3025,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // we have to undo the SPARQL-escaping of the input. loadedClassDef: ClassInfoContentV2 <- OntologyHelpers.loadClassDefinition( - settings, appActor, classIri = internalClassIri ) @@ -3156,7 +3118,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read its metadata. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deletePropertyCommentRequest.lastModificationDate @@ -3203,7 +3164,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings = settings, appActor = appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -3211,7 +3171,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the update was successful. loadedPropertyDef: PropertyInfoContentV2 <- OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = internalPropertyIri ) @@ -3232,7 +3191,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon maybeLoadedLinkValuePropertyDefFuture: Option[Future[PropertyInfoContentV2]] = maybeLinkValueOfPropertyToUpdate.map { linkValueReadPropertyInfo: ReadPropertyInfoV2 => OntologyHelpers.loadPropertyDefinition( - settings, appActor, propertyIri = linkValueReadPropertyInfo.entityInfoContent.propertyIri ) @@ -3375,7 +3333,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology exists and has not been updated by another user since the client last read its metadata. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deleteClassCommentRequest.lastModificationDate @@ -3398,7 +3355,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings = settings, appActor = appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -3406,7 +3362,6 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon // Check that the update was successful. loadedClassDef: ClassInfoContentV2 <- OntologyHelpers.loadClassDefinition( - settings, appActor, classIri = internalClassIri ) 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 314465d790..e0f220b7c0 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 @@ -60,7 +60,6 @@ object CardinalityHandler { // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deleteCardinalitiesFromClassRequest.lastModificationDate @@ -220,7 +219,6 @@ object CardinalityHandler { // Check that the ontology exists and has not been updated by another user since the client last read it. _ <- OntologyHelpers.checkOntologyLastModificationDateBeforeUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = deleteCardinalitiesFromClassRequest.lastModificationDate @@ -403,7 +401,6 @@ object CardinalityHandler { // Check that the ontology's last modification date was updated. _ <- OntologyHelpers.checkOntologyLastModificationDateAfterUpdate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = currentTime @@ -412,7 +409,6 @@ object CardinalityHandler { // Check that the data that was saved corresponds to the data that was submitted. loadedClassDef <- OntologyHelpers.loadClassDefinition( - settings, appActor, classIri = internalClassIri ) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala index 9b7c6dd02d..b2b1f2f075 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala @@ -39,7 +39,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality._ import org.knora.webapi.messages.v2.responder.ontologymessages._ import org.knora.webapi.messages.v2.responder.standoffmessages.StandoffDataTypeClasses import org.knora.webapi.responders.v2.ontology.Cache.OntologyCacheData -import org.knora.webapi.settings.KnoraSettingsImpl object OntologyHelpers { @@ -125,7 +124,6 @@ object OntologyHelpers { * @return an [[OntologyMetadataV2]], or [[None]] if the ontology is not found. */ def loadOntologyMetadata( - settings: KnoraSettingsImpl, appActor: ActorRef, internalOntologyIri: SmartIri )(implicit @@ -1018,10 +1016,11 @@ object OntologyHelpers { /** * Gets the set of subjects that refer to an ontology or its entities. * + * @param appActor the store manager actor ref. * @param ontology the ontology. * @return the set of subjects that refer to the ontology or its entities. */ - def getSubjectsUsingOntology(settings: KnoraSettingsImpl, appActor: ActorRef, ontology: ReadOntologyV2)(implicit + def getSubjectsUsingOntology(appActor: ActorRef, ontology: ReadOntologyV2)(implicit ec: ExecutionContext, timeout: Timeout ): Future[Set[IRI]] = @@ -1070,12 +1069,12 @@ object OntologyHelpers { /** * Loads a property definition from the triplestore and converts it to a [[PropertyInfoContentV2]]. * + * @param appActor the store manager actor ref. * @param propertyIri the IRI of the property to be loaded. * * @return a [[PropertyInfoContentV2]] representing the property definition. */ def loadPropertyDefinition( - settings: KnoraSettingsImpl, appActor: ActorRef, propertyIri: SmartIri )(implicit ex: ExecutionContext, stringFormatter: StringFormatter, timeout: Timeout): Future[PropertyInfoContentV2] = @@ -1515,12 +1514,12 @@ object OntologyHelpers { /** * Loads a class definition from the triplestore and converts it to a [[ClassInfoContentV2]]. * + * @param appActor the store manager actor ref. * @param classIri the IRI of the class to be loaded. * * @return a [[ClassInfoContentV2]] representing the class definition. */ def loadClassDefinition( - settings: KnoraSettingsImpl, appActor: ActorRef, classIri: SmartIri )(implicit ex: ExecutionContext, stringFormatter: StringFormatter, timeout: Timeout): Future[ClassInfoContentV2] = @@ -1702,7 +1701,6 @@ object OntologyHelpers { * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return * an error message fitting for the "before update" case. * - * @param settings the application settings. * @param appActor the store manager actor ref. * @param internalOntologyIri the internal IRI of the ontology. * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. @@ -1710,13 +1708,11 @@ object OntologyHelpers { * @return a failed Future if the expected last modification date is not found. */ def checkOntologyLastModificationDateBeforeUpdate( - settings: KnoraSettingsImpl, appActor: ActorRef, internalOntologyIri: SmartIri, expectedLastModificationDate: Instant )(implicit ec: ExecutionContext, stringFormatter: StringFormatter, timeout: Timeout): Future[Unit] = checkOntologyLastModificationDate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = expectedLastModificationDate, @@ -1729,7 +1725,6 @@ object OntologyHelpers { * Checks that the last modification date of an ontology is the same as the one we expect it to be. If not, return * an error message fitting for the "after update" case. * - * @param settings the application settings. * @param appActor the store manager actor ref. * @param internalOntologyIri the internal IRI of the ontology. * @param expectedLastModificationDate the last modification date that should now be attached to the ontology. @@ -1737,13 +1732,11 @@ object OntologyHelpers { * @return a failed Future if the expected last modification date is not found. */ def checkOntologyLastModificationDateAfterUpdate( - settings: KnoraSettingsImpl, appActor: ActorRef, internalOntologyIri: SmartIri, expectedLastModificationDate: Instant )(implicit ec: ExecutionContext, stringFormatter: StringFormatter, timeout: Timeout): Future[Unit] = checkOntologyLastModificationDate( - settings, appActor, internalOntologyIri = internalOntologyIri, expectedLastModificationDate = expectedLastModificationDate, @@ -1755,7 +1748,6 @@ object OntologyHelpers { /** * Checks that the last modification date of an ontology is the same as the one we expect it to be. * - * @param settings the application settings. * @param appActor the store manager actor ref. * @param internalOntologyIri the internal IRI of the ontology. * @param expectedLastModificationDate the last modification date that the ontology is expected to have. @@ -1764,7 +1756,6 @@ object OntologyHelpers { * @return a failed Future if the expected last modification date is not found. */ private def checkOntologyLastModificationDate( - settings: KnoraSettingsImpl, appActor: ActorRef, internalOntologyIri: SmartIri, expectedLastModificationDate: Instant, @@ -1772,7 +1763,6 @@ object OntologyHelpers { )(implicit ec: ExecutionContext, stringFormatter: StringFormatter, timeout: Timeout): Future[Unit] = for { existingOntologyMetadata: Option[OntologyMetadataV2] <- loadOntologyMetadata( - settings, appActor, internalOntologyIri = internalOntologyIri ) diff --git a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala index 6aaf53aa6a..202e15d235 100644 --- a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala +++ b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala @@ -163,9 +163,9 @@ class KnoraSettingsImpl(config: Config, log: Logger) extends Extension { else "") val sipiFileServerPrefix: String = config.getString("app.sipi.file-server-path") val externalSipiIIIFGetUrl: String = externalSipiBaseUrl - val sipiFileMetadataRouteV2: String = config.getString("app.sipi.v2.file-metadata-route") - val sipiMoveFileRouteV2: String = config.getString("app.sipi.v2.move-file-route") - val sipiDeleteTempFileRouteV2: String = config.getString("app.sipi.v2.delete-temp-file-route") + val sipiFileMetadataRouteV2: String = config.getString("app.sipi.file-metadata-route") + val sipiMoveFileRouteV2: String = config.getString("app.sipi.move-file-route") + val sipiDeleteTempFileRouteV2: String = config.getString("app.sipi.delete-temp-file-route") val arkResolver: String = config.getString("app.ark.resolver") val arkAssignedNumber: Int = config.getInt("app.ark.assigned-number") @@ -193,8 +193,8 @@ class KnoraSettingsImpl(config: Config, log: Logger) extends Extension { val showInternalErrors: Boolean = config.getBoolean("app.show-internal-errors") val maxResultsPerSearchResultPage: Int = config.getInt("app.max-results-per-search-result-page") val standoffPerPage: Int = config.getInt("app.standoff-per-page") - val defaultIconSizeDimX: Int = config.getInt("app.gui.default-icon-size.dimX") - val defaultIconSizeDimY: Int = config.getInt("app.gui.default-icon-size.dimY") + val defaultIconSizeDimX: Int = config.getInt("app.gui.default-icon-size.dim-x") + val defaultIconSizeDimY: Int = config.getInt("app.gui.default-icon-size.dim-y") val v2ResultsPerPage: Int = config.getInt("app.v2.resources-sequence.results-per-page") val searchValueMinLength: Int = config.getInt("app.v2.fulltext-search.search-value-min-length") diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala index 5d100ae67b..9ef62f18b6 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala @@ -103,7 +103,7 @@ case class IIIFServiceSipiImpl( // builds the url for the operation def moveFileUrl(token: String) = - ZIO.succeed(s"${config.sipi.internalBaseUrl}/${config.sipi.v2.moveFileRoute}?token=$token") + ZIO.succeed(s"${config.sipi.internalBaseUrl}/${config.sipi.moveFileRoute}?token=$token") // build the form to send together with the request val formParams = new util.ArrayList[NameValuePair]() @@ -149,7 +149,7 @@ case class IIIFServiceSipiImpl( def deleteUrl(token: String): ZIO[Any, Nothing, String] = ZIO.succeed( - s"${config.sipi.internalBaseUrl}/${config.sipi.v2.deleteTempFileRoute}/${deleteTemporaryFileRequestV2.internalFilename}?token=$token" + s"${config.sipi.internalBaseUrl}/${config.sipi.deleteTempFileRoute}/${deleteTemporaryFileRequestV2.internalFilename}?token=$token" ) for { From 83ec9bf19cda74febd55c97159abf99e3a22240e Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 13:52:57 +0200 Subject: [PATCH 03/44] fix deprecation warnings in GravsearchParser --- .../search/gravsearch/GravsearchParser.scala | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala index ea9ee62227..85ee164b68 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/GravsearchParser.scala @@ -112,22 +112,12 @@ object GravsearchParser { * @param sourceName the source name. * @return an [[algebra.Var]] representing the name or its literal value. */ - def nameToVar(sourceName: String): algebra.Var = { - val sparqlVar = new algebra.Var - sparqlVar.setName(sourceName) - + def nameToVar(sourceName: String): algebra.Var = valueConstants.get(sourceName) match { - case Some(valueConstant) => - sparqlVar.setConstant(true) - sparqlVar.setValue(valueConstant.getValue) - - case None => - sparqlVar.setConstant(false) + case Some(valueConstant) => new algebra.Var(sourceName, valueConstant.getValue(), false, true) + case None => new algebra.Var(sourceName, null, false, false) } - sparqlVar - } - // Convert each ConstructStatementWithConstants to a StatementPattern for use in the CONSTRUCT clause. val constructStatements: Seq[StatementPattern] = constructStatementsWithConstants.toVector.map { (constructStatementWithConstant: ConstructStatementWithConstants) => From a45c32ec78b3d98ea83abbddaea4d08b7426cdb2 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 13:55:44 +0200 Subject: [PATCH 04/44] update login form --- .../main/scala/org/knora/webapi/routing/Authenticator.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2b5ee161b3..3aa6bb9aa8 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala @@ -199,7 +199,7 @@ trait Authenticator extends InstrumentationSupport { |
|
| """.stripMargin From 497b384b0775b98052485f46b62a204d7504fcce Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 14:00:28 +0200 Subject: [PATCH 05/44] remove settings from CardinalityHandler --- .../webapi/responders/v2/OntologyResponderV2.scala | 2 -- .../responders/v2/ontology/CardinalityHandler.scala | 9 --------- .../responders/v2/ontology/CardinalitiesSpec.scala | 10 +++++----- 3 files changed, 5 insertions(+), 16 deletions(-) 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 2d66e2b7c4..4bfed78a95 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 @@ -1756,7 +1756,6 @@ class OntologyResponderV2(responderData: ResponderData, config: AppConfig) exten iri = ONTOLOGY_CACHE_LOCK_IRI, task = () => CardinalityHandler.canDeleteCardinalitiesFromClass( - settings, appActor, deleteCardinalitiesFromClassRequest = canDeleteCardinalitiesFromClassRequest, internalClassIri = internalClassIri, @@ -1796,7 +1795,6 @@ class OntologyResponderV2(responderData: ResponderData, config: AppConfig) exten iri = ONTOLOGY_CACHE_LOCK_IRI, task = () => CardinalityHandler.deleteCardinalitiesFromClass( - settings, appActor, deleteCardinalitiesFromClassRequest = deleteCardinalitiesFromClassRequest, internalClassIri = internalClassIri, 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 e0f220b7c0..c92083a577 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 @@ -28,7 +28,6 @@ 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.settings.KnoraSettingsImpl /** * Contains methods used for dealing with cardinalities on a class @@ -38,7 +37,6 @@ object CardinalityHandler { /** * FIXME(DSP-1856): Only works if a single cardinality is supplied. * - * @param settings the applications settings. * @param storeManager the store manager actor. * @param deleteCardinalitiesFromClassRequest the requested cardinalities to be deleted. * @param internalClassIri the Class from which the cardinalities are deleted. @@ -46,7 +44,6 @@ object CardinalityHandler { * @return a [[CanDoResponseV2]] indicating whether a class's cardinalities can be deleted. */ def canDeleteCardinalitiesFromClass( - settings: KnoraSettingsImpl, appActor: ActorRef, deleteCardinalitiesFromClassRequest: CanDeleteCardinalitiesFromClassRequestV2, internalClassIri: SmartIri, @@ -124,7 +121,6 @@ object CardinalityHandler { submittedPropertyToDelete: SmartIri = cardinalitiesToDelete.head._1 propertyIsUsed: Boolean <- isPropertyUsedInResources( - settings, appActor, internalClassIri, submittedPropertyToDelete @@ -197,7 +193,6 @@ 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 settings the applications settings. * @param storeManager the store manager actor. * @param deleteCardinalitiesFromClassRequest the requested cardinalities to be deleted. * @param internalClassIri the Class from which the cardinalities are deleted. @@ -205,7 +200,6 @@ object CardinalityHandler { * @return a [[ReadOntologyV2]] in the internal schema, containing the new class definition. */ def deleteCardinalitiesFromClass( - settings: KnoraSettingsImpl, appActor: ActorRef, deleteCardinalitiesFromClassRequest: DeleteCardinalitiesFromClassRequestV2, internalClassIri: SmartIri, @@ -284,7 +278,6 @@ object CardinalityHandler { submittedPropertyToDelete: SmartIri = cardinalitiesToDelete.head._1 propertyIsUsed: Boolean <- isPropertyUsedInResources( - settings, appActor, internalClassIri, submittedPropertyToDelete @@ -444,7 +437,6 @@ 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 settings application settings. * @param storeManager store manager actor ref. * @param internalPropertyIri the IRI of the entity that is being checked for usage. * @param ec the execution context onto with the future will run. @@ -452,7 +444,6 @@ object CardinalityHandler { * @return a [[Boolean]] denoting if the property entity is used. */ def isPropertyUsedInResources( - settings: KnoraSettingsImpl, appActor: ActorRef, internalClassIri: SmartIri, internalPropertyIri: SmartIri diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala index a02ab6903e..a02129ff0e 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala @@ -29,7 +29,7 @@ class CardinalitiesSpec extends CoreSpec { val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toOntologySchema(InternalSchema) println(s"internalPropertyIri: $internalPropertyIri") - val resF = CardinalityHandler.isPropertyUsedInResources(settings, appActor, internalClassIri, internalPropertyIri) + val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) resF map { res => println(res); assert(res, "property is used in resource (instance of that resource class)") } } @@ -38,7 +38,7 @@ class CardinalitiesSpec extends CoreSpec { val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTestResourceClass").toOntologySchema(InternalSchema) println(s"internalPropertyIri: $internalPropertyIri") - val resF = CardinalityHandler.isPropertyUsedInResources(settings, appActor, internalClassIri, internalPropertyIri) + val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) resF map { res => println(res); assert(!res, "property is not used in resource (instance of that resource class)") } @@ -49,7 +49,7 @@ class CardinalitiesSpec extends CoreSpec { val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toOntologySchema(InternalSchema) println(s"internalPropertyIri: $internalPropertyIri") - val resF = CardinalityHandler.isPropertyUsedInResources(settings, appActor, internalClassIri, internalPropertyIri) + val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) resF map { res => println(res); assert(!res, "property is not used in resource (instance of that resource class)") } @@ -61,7 +61,7 @@ class CardinalitiesSpec extends CoreSpec { val internalClassIri = anythingOntologyIri.makeEntityIri("Thing").toOntologySchema(InternalSchema) println(s"internalPropertyIri: $internalPropertyIri") - val resF = CardinalityHandler.isPropertyUsedInResources(settings, appActor, internalClassIri, internalPropertyIri) + val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) resF map { res => println(res); assert(res, "property is used in resource (instance of resource class)") } } @@ -70,7 +70,7 @@ class CardinalitiesSpec extends CoreSpec { val internalClassIri = freetestOntologyIri.makeEntityIri("FreeTest").toOntologySchema(InternalSchema) println(s"internalPropertyIri: $internalPropertyIri") - val resF = CardinalityHandler.isPropertyUsedInResources(settings, appActor, internalClassIri, internalPropertyIri) + val resF = CardinalityHandler.isPropertyUsedInResources(appActor, internalClassIri, internalPropertyIri) resF map { res => println(res); assert(res, "property is used in a resource of subclass") } } From 8836777056f01bf8159d5e6b3223f9f5c504d33c Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 14:05:21 +0200 Subject: [PATCH 06/44] remove settings from Cache --- .../org/knora/webapi/responders/v2/OntologyResponderV2.scala | 2 +- .../scala/org/knora/webapi/responders/v2/ontology/Cache.scala | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) 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 4bfed78a95..0cabf9bf6b 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 @@ -68,7 +68,7 @@ class OntologyResponderV2(responderData: ResponderData, config: AppConfig) exten */ def receive(msg: OntologiesResponderRequestV2) = msg match { case LoadOntologiesRequestV2(requestingUser) => - Cache.loadOntologies(settings, appActor, requestingUser) + Cache.loadOntologies(appActor, requestingUser) case EntityInfoGetRequestV2(classIris, propertyIris, requestingUser) => getEntityInfoResponseV2(classIris, propertyIris, requestingUser) case StandoffEntityInfoGetRequestV2(standoffClassIris, standoffPropertyIris, requestingUser) => diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/Cache.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/Cache.scala index 2e8cd17efa..5305d35dd1 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/Cache.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/Cache.scala @@ -47,7 +47,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV2 import org.knora.webapi.responders.v2.ontology.OntologyHelpers.OntologyGraph -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.util.cache.CacheUtil object Cache extends LazyLogging { @@ -103,7 +102,6 @@ object Cache extends LazyLogging { * @return a [[SuccessResponseV2]]. */ def loadOntologies( - settings: KnoraSettingsImpl, appActor: ActorRef, requestingUser: UserADM )(implicit ec: ExecutionContext, stringFormat: StringFormatter, timeout: Timeout): Future[SuccessResponseV2] = { From 05a0a8a2f3fab4bde337c6a7d7fbf4e388902f0b Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 14:46:25 +0200 Subject: [PATCH 07/44] remove config from V1 responders --- .../org/knora/webapi/config/AppConfig.scala | 6 ++-- .../webapi/core/actors/RoutingActor.scala | 10 +++---- .../webapi/messages/util/ValueUtilV1.scala | 20 ++++++------- .../responders/admin/ListsResponderADM.scala | 5 ++-- .../responders/admin/StoresResponderADM.scala | 5 ++-- .../responders/admin/UsersResponderADM.scala | 9 ++++-- .../responders/v1/ListsResponderV1.scala | 7 +++-- .../responders/v1/OntologyResponderV1.scala | 25 ++++++++-------- .../responders/v1/ResourcesResponderV1.scala | 29 ++++++++++--------- .../responders/v1/SearchResponderV1.scala | 27 ++++++++--------- .../responders/v1/UsersResponderV1.scala | 9 +++--- .../responders/v1/ValuesResponderV1.scala | 5 ++-- .../responders/v2/OntologyResponderV2.scala | 4 +-- 13 files changed, 85 insertions(+), 76 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index cff4e65c2a..2927e642b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -35,7 +35,8 @@ final case class AppConfig( triplestore: Triplestore, v2: V2, shacl: Shacl, - user: User + fallbackLanguage: String, + maxResultsPerSearchResultPage: Int ) { val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity) val defaultTimeoutAsDuration = @@ -176,9 +177,6 @@ final case class Shacl( ) { val shapesDirPath = Paths.get(shapesDir) } -final case class User( - defaultLanguage: String = "en" -) /** * Loads the applicaton configuration using ZIO-Config. ZIO-Config is capable of loading 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 3824fe361a..04fb842610 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 @@ -114,9 +114,9 @@ class RoutingActor( 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 listsResponderV1: ListsResponderV1 = new ListsResponderV1(responderData, appConfig) val searchResponderV1: SearchResponderV1 = new SearchResponderV1(responderData) - val ontologyResponderV1: OntologyResponderV1 = new OntologyResponderV1(responderData) + val ontologyResponderV1: OntologyResponderV1 = new OntologyResponderV1(responderData, appConfig) val projectsResponderV1: ProjectsResponderV1 = new ProjectsResponderV1(responderData) // V2 responders @@ -129,11 +129,11 @@ class RoutingActor( // Admin responders val groupsResponderADM: GroupsResponderADM = new GroupsResponderADM(responderData) - val listsResponderADM: ListsResponderADM = new ListsResponderADM(responderData) + val listsResponderADM: ListsResponderADM = new ListsResponderADM(responderData, appConfig) 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 storeResponderADM: StoresResponderADM = new StoresResponderADM(responderData, appConfig) + val usersResponderADM: UsersResponderADM = new UsersResponderADM(responderData, appConfig) val sipiRouterADM: SipiResponderADM = new SipiResponderADM(responderData) def receive: Receive = { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala index 7e9c3625c3..c97ab7d6f9 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala @@ -29,12 +29,12 @@ import org.knora.webapi.messages.v1.responder.resourcemessages.ResourceCreateVal import org.knora.webapi.messages.v1.responder.resourcemessages.ResourceCreateValueResponseV1 import org.knora.webapi.messages.v1.responder.valuemessages._ import org.knora.webapi.messages.v2.responder.standoffmessages._ -import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.config.AppConfig /** * Converts data from SPARQL query results into [[ApiValueV1]] objects. */ -class ValueUtilV1(private val settings: KnoraSettingsImpl) { +class ValueUtilV1(appConfig: AppConfig) { private val stringFormatter = StringFormatter.getGeneralInstance @@ -83,7 +83,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { } def makeSipiImagePreviewGetUrlFromFilename(projectShortcode: String, filename: String): String = - s"${settings.externalSipiIIIFGetUrl}/$projectShortcode/$filename/full/!128,128/0/default.jpg" + s"${appConfig.sipi.externalBaseUrl}/$projectShortcode/$filename/full/!128,128/0/default.jpg" /** * Creates a IIIF URL for accessing an image file via Sipi. @@ -92,7 +92,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { * @return a Sipi IIIF URL. */ def makeSipiImageGetUrlFromFilename(imageFileValueV1: StillImageFileValueV1): String = - s"${settings.externalSipiIIIFGetUrl}/${imageFileValueV1.projectShortcode}/${imageFileValueV1.internalFilename}/full/${imageFileValueV1.dimX},${imageFileValueV1.dimY}/0/default.jpg" + s"${appConfig.sipi.externalBaseUrl}/${imageFileValueV1.projectShortcode}/${imageFileValueV1.internalFilename}/full/${imageFileValueV1.dimX},${imageFileValueV1.dimY}/0/default.jpg" /** * Creates a URL for accessing a document file via Sipi. @@ -101,7 +101,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { * @return a Sipi URL. */ def makeSipiDocumentGetUrlFromFilename(documentFileValueV1: DocumentFileValueV1): String = - s"${settings.externalSipiIIIFGetUrl}/${documentFileValueV1.projectShortcode}/${documentFileValueV1.internalFilename}/file" + s"${appConfig.sipi.externalBaseUrl}/${documentFileValueV1.projectShortcode}/${documentFileValueV1.internalFilename}/file" /** * Creates a URL for accessing a archive file via Sipi. @@ -110,7 +110,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { * @return a Sipi URL. */ def makeSipiArchiveGetUrlFromFilename(archiveFileValueV1: ArchiveFileValueV1): String = - s"${settings.externalSipiIIIFGetUrl}/${archiveFileValueV1.projectShortcode}/${archiveFileValueV1.internalFilename}/file" + s"${appConfig.sipi.externalBaseUrl}/${archiveFileValueV1.projectShortcode}/${archiveFileValueV1.internalFilename}/file" /** * Creates a URL for accessing a text file via Sipi. @@ -119,7 +119,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { * @return a Sipi URL. */ def makeSipiTextFileGetUrlFromFilename(textFileValue: TextFileValueV1): String = - s"${settings.externalSipiBaseUrl}/${textFileValue.projectShortcode}/${textFileValue.internalFilename}" + s"${appConfig.sipi.externalBaseUrl}/${textFileValue.projectShortcode}/${textFileValue.internalFilename}" /** * Creates a URL for accessing an audio file via Sipi. @@ -128,7 +128,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { * @return a Sipi URL. */ def makeSipiAudioFileGetUrlFromFilename(audioFileValue: AudioFileValueV1): String = - s"${settings.externalSipiIIIFGetUrl}/${audioFileValue.projectShortcode}/${audioFileValue.internalFilename}/file" + s"${appConfig.sipi.externalBaseUrl}/${audioFileValue.projectShortcode}/${audioFileValue.internalFilename}/file" /** * Creates a URL for accessing a video file via Sipi. @@ -137,7 +137,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { * @return a Sipi URL. */ def makeSipiVideoFileGetUrlFromFilename(videoFileValue: MovingImageFileValueV1): String = - s"${settings.externalSipiIIIFGetUrl}/${videoFileValue.projectShortcode}/${videoFileValue.internalFilename}/file" + s"${appConfig.sipi.externalBaseUrl}/${videoFileValue.projectShortcode}/${videoFileValue.internalFilename}/file" // A Map of MIME types to Knora API v1 binary format name. private val mimeType2V1Format = new ErrorHandlingMap( @@ -251,7 +251,7 @@ class ValueUtilV1(private val settings: KnoraSettingsImpl) { resourceClassIri.substring(resourceClassIri.lastIndexOf('/') + 1, resourceClassIri.lastIndexOf('#')) // create URL: combine salsah-address and port, project icons base path, ontology name, icon name - settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + ontologyName + '/' + iconsSrc + appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + ontologyName + '/' + iconsSrc } /** 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 b9a9997922..6eacb877cb 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 @@ -34,11 +34,12 @@ 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 +import org.knora.webapi.config.AppConfig /** * A responder that returns information about hierarchical lists. */ -class ListsResponderADM(responderData: ResponderData) extends Responder(responderData) { +class ListsResponderADM(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { // The IRI used to lock user creation and update private val LISTS_GLOBAL_LOCK_IRI = "http://rdfh.ch/lists" @@ -798,7 +799,7 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde .getNodePath( queryNodeIri = queryNodeIri, preferredLanguage = requestingUser.lang, - fallbackLanguage = settings.fallbackLanguage + fallbackLanguage = appConfig.fallbackLanguage ) .toString() } 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 87cfdd8672..d07a227999 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 @@ -24,12 +24,13 @@ 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 +import org.knora.webapi.config.AppConfig /** * 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, appConfig: AppConfig) extends Responder(responderData) { /** * A user representing the Knora API server, used in those cases where a user is required. @@ -63,7 +64,7 @@ class StoresResponderADM(responderData: ResponderData) extends Responder(respond for { // FIXME: need to call directly into the State service - value: Boolean <- FastFuture.successful(settings.allowReloadOverHTTP) + value: Boolean <- FastFuture.successful(appConfig.allowReloadOverHttp) _ = if (!value) { throw ForbiddenException( "The ResetTriplestoreContent operation is not allowed. Did you start the server with the right flag?" 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 211adb15cf..f42ebd290c 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 @@ -40,11 +40,14 @@ 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 +import org.knora.webapi.config.AppConfig /** * Provides information about Knora users to other responders. */ -class UsersResponderADM(responderData: ResponderData) extends Responder(responderData) with InstrumentationSupport { +class UsersResponderADM(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData) + with InstrumentationSupport { // The IRI used to lock user creation and update private val USERS_GLOBAL_LOCK_IRI = "http://rdfh.ch/users" @@ -499,7 +502,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } // hash the new password - encoder = new BCryptPasswordEncoder(settings.bcryptPasswordStrength) + encoder = new BCryptPasswordEncoder(appConfig.bcryptPasswordStrength) newHashedPassword = Password .make(encoder.encode(userUpdatePasswordPayload.newPassword.value)) .fold(e => throw e.head, value => value) @@ -1673,7 +1676,7 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde userIri: IRI <- checkOrCreateEntityIri(customUserIri, stringFormatter.makeRandomPersonIri) // hash password - encoder = new BCryptPasswordEncoder(settings.bcryptPasswordStrength) + encoder = new BCryptPasswordEncoder(appConfig.bcryptPasswordStrength) hashedPassword = encoder.encode(userCreatePayloadADM.password.value) // Create the new user. 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 ddba0610f3..384b777c23 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 @@ -20,11 +20,12 @@ 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 +import org.knora.webapi.config.AppConfig /** * A responder that returns information about hierarchical lists. */ -class ListsResponderV1(responderData: ResponderData) extends Responder(responderData) { +class ListsResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { /** * Receives a message of type [[ListsResponderRequestV1]], and returns an appropriate response message. @@ -147,7 +148,7 @@ class ListsResponderV1(responderData: ResponderData) extends Responder(responder .getList( rootNodeIri = rootNodeIri, preferredLanguage = userProfile.userData.lang, - fallbackLanguage = settings.fallbackLanguage + fallbackLanguage = appConfig.fallbackLanguage ) .toString() } @@ -229,7 +230,7 @@ class ListsResponderV1(responderData: ResponderData) extends Responder(responder .getNodePath( queryNodeIri = queryNodeIri, preferredLanguage = userProfile.userData.lang, - fallbackLanguage = settings.fallbackLanguage + fallbackLanguage = appConfig.fallbackLanguage ) .toString() } 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 89ff42aad7..1572054fd7 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 @@ -28,6 +28,7 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.Kn import org.knora.webapi.messages.v2.responder.ontologymessages._ import org.knora.webapi.responders.Responder import org.knora.webapi.responders.Responder.handleUnexpectedMessage +import org.knora.webapi.config.AppConfig /** * Handles requests for information about ontology entities. @@ -35,9 +36,9 @@ 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, appConfig: AppConfig) extends Responder(responderData) { - private val valueUtilV1 = new ValueUtilV1(settings) + private val valueUtilV1 = new ValueUtilV1(appConfig) /** * Receives a message extending [[OntologyResponderRequestV1]], and returns an appropriate response message. @@ -240,14 +241,14 @@ class OntologyResponderV1(responderData: ResponderData) extends Responder(respon predicateIri = OntologyConstants.Rdfs.Label, preferredLangs = Some( userProfile.lang, - settings.fallbackLanguage + appConfig.fallbackLanguage ) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, preferredLangs = Some( userProfile.lang, - settings.fallbackLanguage + appConfig.fallbackLanguage ) ), vocabulary = entityInfo.ontologyIri, @@ -290,14 +291,14 @@ class OntologyResponderV1(responderData: ResponderData) extends Responder(respon predicateIri = OntologyConstants.Rdfs.Label, preferredLangs = Some( userProfile.lang, - settings.fallbackLanguage + appConfig.fallbackLanguage ) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, preferredLangs = Some( userProfile.lang, - settings.fallbackLanguage + appConfig.fallbackLanguage ) ), vocabulary = entityInfo.ontologyIri, @@ -340,11 +341,11 @@ class OntologyResponderV1(responderData: ResponderData) extends Responder(respon name = resourceTypeIri, label = resourceClassInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), description = resourceClassInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), iconsrc = resourceClassInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon), @@ -620,11 +621,11 @@ class OntologyResponderV1(responderData: ResponderData) extends Responder(respon name = propertyIri, label = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), vocabulary = entityInfo.ontologyIri, valuetype_id = OntologyConstants.KnoraBase.LinkValue, @@ -652,11 +653,11 @@ class OntologyResponderV1(responderData: ResponderData) extends Responder(respon name = propertyIri, label = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), vocabulary = entityInfo.ontologyIri, valuetype_id = entityInfo 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 623aad6735..9842f146b1 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 @@ -54,14 +54,15 @@ 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 +import org.knora.webapi.config.AppConfig /** * 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, appConfig: AppConfig) extends Responder(responderData) { // Converts SPARQL query results to ApiValueV1 objects. - private val valueUtilV1 = new ValueUtilV1(settings) + private val valueUtilV1 = new ValueUtilV1(appConfig) /** * Receives a message extending [[ResourcesResponderRequestV1]], and returns an appropriate response message. @@ -436,7 +437,7 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo .getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, preferredLangs = - Some(graphDataGetRequest.userADM.lang, settings.fallbackLanguage) + Some(graphDataGetRequest.userADM.lang, appConfig.fallbackLanguage) ) .getOrElse( throw InconsistentRepositoryDataException( @@ -460,7 +461,7 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo .getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, preferredLangs = - Some(graphDataGetRequest.userADM.lang, settings.fallbackLanguage) + Some(graphDataGetRequest.userADM.lang, appConfig.fallbackLanguage) ) .getOrElse( throw InconsistentRepositoryDataException( @@ -766,14 +767,14 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo resourceClassLabel = resourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ) resInfo: ResourceInfoV1 = resInfoWithoutQueryingOntology.copy( restype_label = resourceClassLabel, restype_description = resourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), restype_iconsrc = maybeResourceTypeIconSrc ) @@ -796,11 +797,11 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo resinfo = incoming.resinfo.copy( restype_label = incomingResourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), restype_description = incomingResourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), restype_iconsrc = incomingResourceTypeEntityInfo.getPredicateObject( OntologyConstants.KnoraBase.ResourceIcon @@ -872,7 +873,7 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo ), label = propertyEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), occurrence = Some(propsAndCardinalities(propertyIri).cardinality.value), attributes = (propertyEntityInfo.getPredicateStringObjectsWithoutLang( @@ -903,7 +904,7 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo ), label = propertyEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ), occurrence = Some(propsAndCardinalities(propertyIri).cardinality.value), attributes = propertyEntityInfo @@ -3096,11 +3097,11 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo entityInfo = entityInfoResponse.resourceClassInfoMap(resTypeIri) label = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ) description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ) iconsrc = entityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) match { case Some(resClassIcon) => @@ -3227,7 +3228,7 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo label = propertyEntityInfo.flatMap( _.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ) ), occurrence = propertyCardinality.map(_.cardinality.value), @@ -3375,7 +3376,7 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo case Some(referencedResTypeEntityInfo) => val labelOption: Option[String] = referencedResTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) ) val resIconOption: Option[String] = referencedResTypeEntityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) 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 3b4796e1b4..e452b4c431 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 @@ -30,12 +30,13 @@ 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 +import org.knora.webapi.config.AppConfig /** * 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, appConfig: AppConfig) extends Responder(responderData) { // 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 @@ -133,7 +134,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde valuePermissionCode: Option[Int] ) - val valueUtilV1 = new ValueUtilV1(settings) + val valueUtilV1 = new ValueUtilV1(appConfig) /** * Receives a message of type [[SearchResponderRequestV1]], and returns an appropriate response message. @@ -166,7 +167,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde .searchFulltext( searchTerms = LuceneQueryString(searchGetRequest.searchValue), preferredLanguage = searchGetRequest.userProfile.lang, - fallbackLanguage = settings.fallbackLanguage, + fallbackLanguage = appConfig.fallbackLanguage, projectIriOption = searchGetRequest.filterByProject, restypeIriOption = searchGetRequest.filterByRestype ) @@ -228,7 +229,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde val resourceEntityInfo: ClassInfoV1 = entityInfoResponse.resourceClassInfoMap(resourceClassIri) val resourceClassLabel = resourceEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) ) val resourceClassIcon = resourceEntityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) val resourceLabel = firstRowMap.getOrElse( @@ -259,7 +260,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde .propertyInfoMap(propertyIri) .getPredicateObject( OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) ) match { case Some(label) => label case None => @@ -308,11 +309,11 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde value = resourceLabel +: matchingValues.map(_.literal), preview_nx = firstRowMap.get("previewDimX") match { case Some(previewDimX) => previewDimX.toInt - case None => settings.defaultIconSizeDimX + case None => appConfig.gui.defaultIconSize.dimX }, preview_ny = firstRowMap.get("previewDimY") match { case Some(previewDimY) => previewDimY.toInt - case None => settings.defaultIconSizeDimY + case None => appConfig.gui.defaultIconSize.dimY }, rights = resourcePermissionCode ) @@ -578,7 +579,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde .searchExtended( searchCriteria = searchCriteria, preferredLanguage = searchGetRequest.userProfile.lang, - fallbackLanguage = settings.fallbackLanguage, + fallbackLanguage = appConfig.fallbackLanguage, projectIriOption = searchGetRequest.filterByProject, restypeIriOption = searchGetRequest.filterByRestype, ownerIriOption = searchGetRequest.filterByOwner @@ -636,7 +637,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde val resourceEntityInfo = entityInfoResponse.resourceClassInfoMap(resourceClassIri) val resourceClassLabel = resourceEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) ) val resourceClassIcon = resourceEntityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) val resourceLabel = firstRowMap.getOrElse( @@ -701,7 +702,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde .propertyInfoMap(propertyIri) .getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, settings.fallbackLanguage) + preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) ) match { case Some(label) => label case None => @@ -759,11 +760,11 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde value = resourceLabel +: matchingValues.map(_.literal), preview_nx = firstRowMap.get("previewDimX") match { case Some(previewDimX) => previewDimX.toInt - case None => settings.defaultIconSizeDimX + case None => appConfig.gui.defaultIconSize.dimX }, preview_ny = firstRowMap.get("previewDimY") match { case Some(previewDimY) => previewDimY.toInt - case None => settings.defaultIconSizeDimY + case None => appConfig.gui.defaultIconSize.dimY }, rights = resourcePermissionCode ) @@ -830,7 +831,7 @@ class SearchResponderV1(responderData: ResponderData) extends Responder(responde throw BadRequestException("Search limit must be greater than 0") } - if (limit > settings.maxResultsPerSearchResultPage) settings.maxResultsPerSearchResultPage else limit + if (limit > appConfig.maxResultsPerSearchResultPage) appConfig.maxResultsPerSearchResultPage else limit } /** 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 6d56e952b7..88b9e500e4 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 @@ -30,11 +30,12 @@ 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 +import org.knora.webapi.config.AppConfig /** * Provides information about Knora users to other responders. */ -class UsersResponderV1(responderData: ResponderData) extends Responder(responderData) { +class UsersResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { // The IRI used to lock user creation and update val USERS_GLOBAL_LOCK_IRI = "http://rdfh.ch/users" @@ -98,7 +99,7 @@ class UsersResponderV1(responderData: ResponderData) extends Responder(responder UserDataV1( lang = propsMap.get(OntologyConstants.KnoraAdmin.PreferredLanguage) match { case Some(langList) => langList - case None => settings.fallbackLanguage + case None => appConfig.fallbackLanguage }, user_id = Some(userIri), email = propsMap.get(OntologyConstants.KnoraAdmin.Email), @@ -454,7 +455,7 @@ class UsersResponderV1(responderData: ResponderData) extends Responder(responder val userDataV1 = UserDataV1( lang = groupedUserData.get(OntologyConstants.KnoraAdmin.PreferredLanguage) match { case Some(langList) => langList.head - case None => settings.fallbackLanguage + case None => appConfig.fallbackLanguage }, user_id = Some(returnedUserIri), email = groupedUserData.get(OntologyConstants.KnoraAdmin.Email).map(_.head), @@ -494,7 +495,7 @@ class UsersResponderV1(responderData: ResponderData) extends Responder(responder val userDataV1 = UserDataV1( lang = groupedUserData.get(OntologyConstants.KnoraAdmin.PreferredLanguage) match { case Some(langList) => langList.head - case None => settings.fallbackLanguage + case None => appConfig.fallbackLanguage }, user_id = Some(returnedUserIri), email = groupedUserData.get(OntologyConstants.KnoraAdmin.Email).map(_.head), 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 d44f9055bf..f2a2b3124c 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 @@ -50,13 +50,14 @@ 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._ +import org.knora.webapi.config.AppConfig /** * Updates Knora values. */ -class ValuesResponderV1(responderData: ResponderData) extends Responder(responderData) { +class ValuesResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { // Converts SPARQL query results to ApiValueV1 objects. - val valueUtilV1 = new ValueUtilV1(settings) + val valueUtilV1 = new ValueUtilV1(appConfig) /** * Receives a message of type [[ValuesResponderRequestV1]], and returns an appropriate response message. 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 0cabf9bf6b..5b53df6bcc 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 @@ -61,7 +61,7 @@ import org.knora.webapi.config.AppConfig * * The API v1 ontology responder, which is read-only, delegates most of its work to this responder. */ -class OntologyResponderV2(responderData: ResponderData, config: AppConfig) extends Responder(responderData) { +class OntologyResponderV2(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { /** * Receives a message of type [[OntologiesResponderRequestV2]], and returns an appropriate response message. @@ -294,7 +294,7 @@ class OntologyResponderV2(responderData: ResponderData, config: AppConfig) exten label = classInfo.entityInfoContent .getPredicateStringLiteralObject( predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, - preferredLangs = Some(requestingUser.lang, config.user.defaultLanguage) + preferredLangs = Some(requestingUser.lang, appConfig.fallbackLanguage) ) .getOrElse( throw InconsistentRepositoryDataException(s"Resource class $subClassIri has no rdfs:label") From 6db92578097c8d4109b8e1a7e757a3e8abe3a56c Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 15:07:56 +0200 Subject: [PATCH 08/44] remove settings from V1 responders --- .../scala/org/knora/webapi/core/actors/RoutingActor.scala | 8 ++++---- .../org/knora/webapi/messages/util/ValueUtilV1.scala | 2 +- .../knora/webapi/responders/admin/ListsResponderADM.scala | 2 +- .../webapi/responders/admin/StoresResponderADM.scala | 2 +- .../knora/webapi/responders/admin/UsersResponderADM.scala | 2 +- .../org/knora/webapi/responders/v1/ListsResponderV1.scala | 2 +- .../knora/webapi/responders/v1/OntologyResponderV1.scala | 2 +- .../knora/webapi/responders/v1/ResourcesResponderV1.scala | 2 +- .../knora/webapi/responders/v1/SearchResponderV1.scala | 2 +- .../org/knora/webapi/responders/v1/UsersResponderV1.scala | 2 +- .../knora/webapi/responders/v1/ValuesResponderV1.scala | 2 +- .../knora/webapi/responders/v2/OntologyResponderV2.scala | 2 +- webapi/src/test/scala/org/knora/webapi/CoreSpec.scala | 1 + .../webapi/responders/v1/ResourcesResponderV1Spec.scala | 8 +------- 14 files changed, 17 insertions(+), 22 deletions(-) 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 04fb842610..a16ffc8878 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 @@ -110,12 +110,12 @@ class RoutingActor( // V1 responders val ckanResponderV1: CkanResponderV1 = new CkanResponderV1(responderData) - val resourcesResponderV1: ResourcesResponderV1 = new ResourcesResponderV1(responderData) - val valuesResponderV1: ValuesResponderV1 = new ValuesResponderV1(responderData) + val resourcesResponderV1: ResourcesResponderV1 = new ResourcesResponderV1(responderData, appConfig) + val valuesResponderV1: ValuesResponderV1 = new ValuesResponderV1(responderData, appConfig) val standoffResponderV1: StandoffResponderV1 = new StandoffResponderV1(responderData) - val usersResponderV1: UsersResponderV1 = new UsersResponderV1(responderData) + val usersResponderV1: UsersResponderV1 = new UsersResponderV1(responderData, appConfig) val listsResponderV1: ListsResponderV1 = new ListsResponderV1(responderData, appConfig) - val searchResponderV1: SearchResponderV1 = new SearchResponderV1(responderData) + val searchResponderV1: SearchResponderV1 = new SearchResponderV1(responderData, appConfig) val ontologyResponderV1: OntologyResponderV1 = new OntologyResponderV1(responderData, appConfig) val projectsResponderV1: ProjectsResponderV1 = new ProjectsResponderV1(responderData) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala index c97ab7d6f9..e4553794f7 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ValueUtilV1.scala @@ -16,6 +16,7 @@ import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotImplementedException import dsp.errors.OntologyConstraintException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -29,7 +30,6 @@ import org.knora.webapi.messages.v1.responder.resourcemessages.ResourceCreateVal import org.knora.webapi.messages.v1.responder.resourcemessages.ResourceCreateValueResponseV1 import org.knora.webapi.messages.v1.responder.valuemessages._ import org.knora.webapi.messages.v2.responder.standoffmessages._ -import org.knora.webapi.config.AppConfig /** * Converts data from SPARQL query results into [[ApiValueV1]] objects. 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 6eacb877cb..01dd83f0c7 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 @@ -17,6 +17,7 @@ import dsp.valueobjects.Iri._ import dsp.valueobjects.List.ListName import dsp.valueobjects.ListErrorMessages import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -34,7 +35,6 @@ 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 -import org.knora.webapi.config.AppConfig /** * A responder that returns information about hierarchical lists. 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 d07a227999..b643b9232b 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 @@ -11,6 +11,7 @@ import akka.pattern._ import scala.concurrent.Future import dsp.errors.ForbiddenException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.storesmessages.ResetTriplestoreContentRequestADM import org.knora.webapi.messages.admin.responder.storesmessages.ResetTriplestoreContentResponseADM import org.knora.webapi.messages.admin.responder.storesmessages.StoreResponderRequestADM @@ -24,7 +25,6 @@ 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 -import org.knora.webapi.config.AppConfig /** * This responder is used by [[org.knora.webapi.routing.admin.StoreRouteADM]], for piping through HTTP requests to the 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 f42ebd290c..bef127e117 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 @@ -17,6 +17,7 @@ import dsp.errors.InconsistentRepositoryDataException import dsp.errors._ import dsp.valueobjects.User._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.instrumentation.InstrumentationSupport import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants @@ -40,7 +41,6 @@ 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 -import org.knora.webapi.config.AppConfig /** * Provides information about Knora users to other responders. 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 384b777c23..533f03864b 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 @@ -12,6 +12,7 @@ import scala.concurrent.Future import dsp.errors.NotFoundException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.store.triplestoremessages.SparqlSelectRequest import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.rdf.SparqlSelectResult @@ -20,7 +21,6 @@ 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 -import org.knora.webapi.config.AppConfig /** * A responder that returns information about hierarchical lists. 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 1572054fd7..107b4d2207 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 @@ -13,6 +13,7 @@ import dsp.constants.SalsahGui import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM @@ -28,7 +29,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.Kn import org.knora.webapi.messages.v2.responder.ontologymessages._ import org.knora.webapi.responders.Responder import org.knora.webapi.responders.Responder.handleUnexpectedMessage -import org.knora.webapi.config.AppConfig /** * Handles requests for information about ontology entities. 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 9842f146b1..1c3216de2e 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 @@ -19,6 +19,7 @@ import dsp.constants.SalsahGui import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -54,7 +55,6 @@ 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 -import org.knora.webapi.config.AppConfig /** * Responds to requests for information about resources, and returns responses in Knora API v1 format. 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 e452b4c431..1b0863502c 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 @@ -12,6 +12,7 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.store.triplestoremessages.SparqlSelectRequest @@ -30,7 +31,6 @@ 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 -import org.knora.webapi.config.AppConfig /** * Responds to requests for user search queries and returns responses in Knora API 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 88b9e500e4..f4d716aa19 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 @@ -15,6 +15,7 @@ import dsp.errors.ApplicationCacheException import dsp.errors.ForbiddenException import dsp.errors.NotFoundException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionDataGetADM import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsDataADM @@ -30,7 +31,6 @@ 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 -import org.knora.webapi.config.AppConfig /** * Provides information about Knora users to other responders. 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 f2a2b3124c..1e664aeee3 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 @@ -14,6 +14,7 @@ import scala.concurrent.Future import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter @@ -50,7 +51,6 @@ 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._ -import org.knora.webapi.config.AppConfig /** * Updates Knora values. 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 5b53df6bcc..a481234545 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 @@ -15,6 +15,7 @@ import dsp.constants.SalsahGui import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -40,7 +41,6 @@ import org.knora.webapi.responders.v2.ontology.Cache.ONTOLOGY_CACHE_LOCK_IRI import org.knora.webapi.responders.v2.ontology.CardinalityHandler import org.knora.webapi.responders.v2.ontology.OntologyHelpers import org.knora.webapi.util._ -import org.knora.webapi.config.AppConfig /** * Responds to requests dealing with ontologies. diff --git a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala index f280973d0c..183165ddf2 100644 --- a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala @@ -92,6 +92,7 @@ abstract class CoreSpec // needed by some tests val cacheServiceSettings = new CacheServiceSettings(system.settings.config) val responderData = ResponderData(system, appActor, settings, cacheServiceSettings) + val appConfig = config final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala index fd106aa8ac..aafddc2761 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala @@ -6,8 +6,6 @@ package org.knora.webapi.responders.v1 import akka.testkit.ImplicitSender -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory import spray.json.JsValue import java.util.UUID @@ -40,10 +38,6 @@ import org.knora.webapi.util._ * Static data for testing [[ResourcesResponderV1]]. */ object ResourcesResponderV1Spec { - private val config: Config = ConfigFactory.parseString(""" - akka.loglevel = "DEBUG" - akka.stdout-loglevel = "DEBUG" - """.stripMargin) private val ReiseInsHeiligelandThreeValues: ResourceSearchResponseV1 = ResourceSearchResponseV1( resources = Vector( @@ -647,7 +641,7 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { implicit private val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val valueUtilV1 = new ValueUtilV1(settings) + private val valueUtilV1 = new ValueUtilV1(appConfig) override lazy val rdfDataObjects = List( RdfDataObject(path = "test_data/all_data/incunabula-data.ttl", name = "http://www.knora.org/data/0803/incunabula"), From fa3689c251b64bba21cbf981a50e3ba686d18818 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 16:56:12 +0200 Subject: [PATCH 09/44] use appConfig in V2 responders --- webapi/src/main/resources/application.conf | 2 ++ .../scala/org/knora/webapi/config/AppConfig.scala | 4 +++- .../knora/webapi/core/actors/RoutingActor.scala | 8 ++++---- .../webapi/responders/v2/ListsResponderV2.scala | 7 ++++--- .../responders/v2/ResourcesResponderV2.scala | 4 +++- .../webapi/responders/v2/SearchResponderV2.scala | 14 +++++++++----- .../webapi/responders/v2/StandoffResponderV2.scala | 9 +++++---- 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index c89f5d98de..d0b5ba0542 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -267,6 +267,8 @@ app { allow-reload-over-http = false allow-reload-over-http = ${?KNORA_WEBAPI_ALLOW_RELOAD_OVER_HTTP} + fallback-language = "en" + knora-api { // relevant for direct communication inside the knora stack internal-host = "0.0.0.0" diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 2927e642b4..16c5257dc4 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -36,7 +36,8 @@ final case class AppConfig( v2: V2, shacl: Shacl, fallbackLanguage: String, - maxResultsPerSearchResultPage: Int + maxResultsPerSearchResultPage: Int, + standoffPerPage: Int ) { val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity) val defaultTimeoutAsDuration = @@ -117,6 +118,7 @@ final case class ResourcesSequence( final case class GraphRoute( defaultGraphDepth: Int, + maxGraphBreadth: Int, maxGraphDepth: Int ) 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 a16ffc8878..78e8557bc1 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 @@ -121,11 +121,11 @@ class RoutingActor( // V2 responders val ontologiesResponderV2: OntologyResponderV2 = new OntologyResponderV2(responderData, appConfig) - val searchResponderV2: SearchResponderV2 = new SearchResponderV2(responderData) - val resourcesResponderV2: ResourcesResponderV2 = new ResourcesResponderV2(responderData) + val searchResponderV2: SearchResponderV2 = new SearchResponderV2(responderData, appConfig) + val resourcesResponderV2: ResourcesResponderV2 = new ResourcesResponderV2(responderData, appConfig) val valuesResponderV2: ValuesResponderV2 = new ValuesResponderV2(responderData) - val standoffResponderV2: StandoffResponderV2 = new StandoffResponderV2(responderData) - val listsResponderV2: ListsResponderV2 = new ListsResponderV2(responderData) + val standoffResponderV2: StandoffResponderV2 = new StandoffResponderV2(responderData, appConfig) + val listsResponderV2: ListsResponderV2 = new ListsResponderV2(responderData, appConfig) // Admin responders val groupsResponderADM: GroupsResponderADM = new GroupsResponderADM(responderData) 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 6e18d06485..e2d3e62096 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 @@ -10,6 +10,7 @@ import akka.pattern._ import scala.concurrent.Future import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages.ChildNodeInfoGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ListGetRequestADM import org.knora.webapi.messages.admin.responder.listsmessages.ListGetResponseADM @@ -23,7 +24,7 @@ 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, appConfig: AppConfig) extends Responder(responderData) { /** * Receives a message of type [[ListsResponderRequestV2]], and returns an appropriate response message inside a future. @@ -57,7 +58,7 @@ class ListsResponderV2(responderData: ResponderData) extends Responder(responder ) .mapTo[ListGetResponseADM] - } yield ListGetResponseV2(list = listResponseADM.list, requestingUser.lang, settings.fallbackLanguage) + } yield ListGetResponseV2(list = listResponseADM.list, requestingUser.lang, appConfig.fallbackLanguage) /** * Gets a single list node from the triplestore. @@ -80,6 +81,6 @@ class ListsResponderV2(responderData: ResponderData) extends Responder(responder ) ) .mapTo[ChildNodeInfoGetResponseADM] - } yield NodeGetResponseV2(node = nodeResponse.nodeinfo, requestingUser.lang, settings.fallbackLanguage) + } yield NodeGetResponseV2(node = nodeResponse.nodeinfo, requestingUser.lang, appConfig.fallbackLanguage) } 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 df38c49907..ccfc3cc834 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 @@ -18,6 +18,7 @@ import scala.util.Success import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -64,7 +65,8 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util._ -class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithStandoffV2(responderData) { +class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) + extends ResponderWithStandoffV2(responderData) { /* actor materializer needed for http requests */ implicit val materializer: Materializer = Materializer.matFromSystem(system) 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 7c638dd82a..b4b7ade296 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 @@ -18,6 +18,7 @@ import dsp.errors.BadRequestException import dsp.errors.GravsearchException import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -49,7 +50,8 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException import org.knora.webapi.util.ApacheLuceneSupport._ -class SearchResponderV2(responderData: ResponderData) extends ResponderWithStandoffV2(responderData) { +class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) + extends ResponderWithStandoffV2(responderData) { // A Gravsearch type inspection runner. private val gravsearchTypeInspectionRunner = new GravsearchTypeInspectionRunner(appActor, responderData) @@ -93,7 +95,8 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand returnFiles, targetSchema, schemaOptions, - requestingUser + requestingUser, + appConfig ) case GravsearchCountRequestV2(query, requestingUser) => @@ -224,7 +227,8 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand returnFiles: Boolean, targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], - requestingUser: UserADM + requestingUser: UserADM, + appConfig: AppConfig ): Future[ReadResourcesSequenceV2] = { import org.knora.webapi.messages.util.search.FullTextMainQueryGenerator.FullTextSearchConstants @@ -243,8 +247,8 @@ class SearchResponderV2(responderData: ResponderData) extends ResponderWithStand limitToStandoffClass = limitToStandoffClass.map(_.toString), returnFiles = returnFiles, separator = Some(groupConcatSeparator), - limit = settings.v2ResultsPerPage, - offset = offset * settings.v2ResultsPerPage, // determine the actual offset + limit = appConfig.v2.resourcesSequence.resultsPerPage, + offset = offset * appConfig.v2.resourcesSequence.resultsPerPage, // determine the actual offset countQuery = false ) .toString() 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 69748f4485..57cbda95dd 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 @@ -27,6 +27,7 @@ import scala.xml.XML import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -62,7 +63,7 @@ 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, appConfig: AppConfig) extends Responder(responderData) { /* actor materializer needed for http requests */ implicit val materializer: Materializer = Materializer.matFromSystem(system) @@ -98,7 +99,7 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon private val xsltCacheName = "xsltCache" private def getStandoffV2(getStandoffRequestV2: GetStandoffPageRequestV2): Future[GetStandoffResponseV2] = { - val requestMaxStartIndex = getStandoffRequestV2.offset + settings.standoffPerPage - 1 + val requestMaxStartIndex = getStandoffRequestV2.offset + appConfig.standoffPerPage - 1 for { resourceRequestSparql <- Future( @@ -259,7 +260,7 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon } xsltUrl: String = - s"${settings.internalSipiBaseUrl}/${resource.projectADM.shortcode}/${xsltFileValueContent.fileValue.internalFilename}/file" + s"${appConfig.sipi.internalBaseUrl}/${resource.projectADM.shortcode}/${xsltFileValueContent.fileValue.internalFilename}/file" } yield xsltUrl @@ -1281,7 +1282,7 @@ class StandoffResponderV2(responderData: ResponderData) extends Responder(respon val firstTask = GetStandoffTask( resourceIri = getRemainingStandoffFromTextValueRequestV2.resourceIri, valueIri = getRemainingStandoffFromTextValueRequestV2.valueIri, - offset = settings.standoffPerPage, // the offset of the second page + offset = appConfig.standoffPerPage, // the offset of the second page requestingUser = getRemainingStandoffFromTextValueRequestV2.requestingUser ) From fcb5f2ec7c3769d67d87e8306be237f767c39f37 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 17:24:43 +0200 Subject: [PATCH 10/44] replace settings in responders V2 --- .../util/ConstructResponseUtilV2.scala | 31 ++++++++++--------- .../util/standoff/StandoffTagUtilV2.scala | 8 ++--- .../valuemessages/ValueMessagesV2.scala | 6 ++++ .../responders/v2/ResourcesResponderV2.scala | 14 ++++----- .../responders/v2/SearchResponderV2.scala | 10 +++--- .../responders/v2/StandoffResponderV2.scala | 2 +- .../util/ConstructResponseUtilV2Spec.scala | 16 +++++----- 7 files changed, 47 insertions(+), 40 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala index 1d7de7f747..3838ae8b19 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala @@ -19,6 +19,7 @@ import dsp.errors.AssertionException import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotImplementedException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -41,7 +42,6 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.GetStandoffRespon import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.standoffmessages.StandoffTagV2 import org.knora.webapi.messages.v2.responder.valuemessages._ -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.util.ActorUtil object ConstructResponseUtilV2 { @@ -1114,7 +1114,7 @@ object ConstructResponseUtilV2 { * @param versionDate if defined, represents the requested time in the the resources' version history. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. - * @param settings the application's settings. + * @param appConfig the application's configuration. * @param requestingUser the user making the request. * @return a [[LinkValueContentV2]]. */ @@ -1127,7 +1127,7 @@ object ConstructResponseUtilV2 { versionDate: Option[Instant], appActor: ActorRef, targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, requestingUser: UserADM )(implicit stringFormatter: StringFormatter, @@ -1162,7 +1162,7 @@ object ConstructResponseUtilV2 { appActor = appActor, requestingUser = requestingUser, targetSchema = targetSchema, - settings = settings + appConfig = appConfig ) } yield linkValue.copy( nestedResource = Some(nestedResource) @@ -1183,7 +1183,7 @@ object ConstructResponseUtilV2 { * @param versionDate if defined, represents the requested time in the the resources' version history. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. - * @param settings the application's settings. + * @param appConfig the application's configuration. * @param requestingUser the user making the request. * @return a [[ValueContentV2]] representing a value. */ @@ -1195,7 +1195,7 @@ object ConstructResponseUtilV2 { versionDate: Option[Instant] = None, appActor: ActorRef, targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, requestingUser: UserADM )(implicit stringFormatter: StringFormatter, @@ -1344,7 +1344,7 @@ object ConstructResponseUtilV2 { .mapTo[NodeGetResponseV2] } yield listNode.copy( listNodeLabel = nodeResponse.node - .getLabelInPreferredLanguage(userLang = requestingUser.lang, fallbackLang = settings.fallbackLanguage) + .getLabelInPreferredLanguage(userLang = requestingUser.lang, fallbackLang = appConfig.fallbackLanguage) ) case ApiV2Complex => FastFuture.successful(listNode) @@ -1385,7 +1385,7 @@ object ConstructResponseUtilV2 { appActor = appActor, requestingUser = requestingUser, targetSchema = targetSchema, - settings = settings + appConfig = appConfig ) case fileValueClass: IRI if OntologyConstants.KnoraBase.FileValueClasses.contains(fileValueClass) => @@ -1415,7 +1415,7 @@ object ConstructResponseUtilV2 { * @param versionDate if defined, represents the requested time in the the resources' version history. * @param responderManager the Knora responder manager. * @param targetSchema the schema of the response. - * @param settings the application's settings. + * @param appConfig the application's configuration. * @param requestingUser the user making the request. * @return a [[ReadResourceV2]]. */ @@ -1427,7 +1427,7 @@ object ConstructResponseUtilV2 { versionDate: Option[Instant], appActor: ActorRef, targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, requestingUser: UserADM )(implicit stringFormatter: StringFormatter, @@ -1496,7 +1496,7 @@ object ConstructResponseUtilV2 { appActor = appActor, requestingUser = requestingUser, targetSchema = targetSchema, - settings = settings + appConfig = appConfig ) attachedToUser = valObj.requireIriObject(OntologyConstants.KnoraBase.AttachedToUser.toSmartIri) @@ -1607,7 +1607,7 @@ object ConstructResponseUtilV2 { * @param versionDate if defined, represents the requested time in the the resources' version history. * @param responderManager the Knora responder manager. * @param targetSchema the schema of response. - * @param settings the application's settings. + * @param appConfig the application's configuration. * @param requestingUser the user making the request. * @return a collection of [[ReadResourceV2]] representing the search results. */ @@ -1621,7 +1621,7 @@ object ConstructResponseUtilV2 { versionDate: Option[Instant], appActor: ActorRef, targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, requestingUser: UserADM )(implicit stringFormatter: StringFormatter, @@ -1643,7 +1643,7 @@ object ConstructResponseUtilV2 { versionDate = versionDate, appActor = appActor, targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, requestingUser = requestingUser ) }.toVector @@ -1653,7 +1653,8 @@ object ConstructResponseUtilV2 { // If we got a full page of results from the triplestore (before filtering for permissions), there // might be at least one more page of results that the user could request. - mayHaveMoreResults = calculateMayHaveMoreResults && pageSizeBeforeFiltering == settings.v2ResultsPerPage + mayHaveMoreResults = + calculateMayHaveMoreResults && pageSizeBeforeFiltering == appConfig.v2.resourcesSequence.resultsPerPage } yield ReadResourcesSequenceV2( resources = resources, hiddenResourceIris = mainResourcesAndValueRdfData.hiddenResourceIris, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/StandoffTagUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/StandoffTagUtilV2.scala index 088cfc9c76..2ab178266a 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/StandoffTagUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/StandoffTagUtilV2.scala @@ -18,6 +18,7 @@ import scala.concurrent.Future import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -33,7 +34,6 @@ import org.knora.webapi.messages.v1.responder.valuemessages.UpdateValueV1 import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality._ import org.knora.webapi.messages.v2.responder.ontologymessages._ import org.knora.webapi.messages.v2.responder.standoffmessages._ -import org.knora.webapi.settings.KnoraSettingsImpl object StandoffTagUtilV2 { @@ -1666,17 +1666,17 @@ object StandoffTagUtilV2 { * for the first page of standoff in a text value. Otherwise, returns `(None, None)`. * * @param queryStandoff `true` if standoff should be queried. - * @param settings the application settings. + * @param appConfig the application configuration. * @return a tuple containing the minimum start index and maximum start index, or `(None, None)` if standoff * is not being queried with text values. */ def getStandoffMinAndMaxStartIndexesForTextValueQuery( queryStandoff: Boolean, - settings: KnoraSettingsImpl + appConfig: AppConfig ): (Option[Int], Option[Int]) = if (queryStandoff) { // Yes. Get the first page of standoff with each text value. - (Some(0), Some(settings.standoffPerPage - 1)) + (Some(0), Some(appConfig.standoffPerPage - 1)) } else { // No. (None, None) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 003df3ab83..43760196d5 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -3189,13 +3189,18 @@ case class StillImageFileValueContentV2( def makeFileUrl(projectADM: ProjectADM, settings: KnoraSettingsImpl): String = s"${settings.externalSipiIIIFGetUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/full/$dimX,$dimY/0/default.jpg" + // def makeFileUrl(projectADM: ProjectADM, appConfig: AppConfig): String = + // s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/full/$dimX,$dimY/0/default.jpg" + override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, settings: KnoraSettingsImpl, + // appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { val fileUrl: String = makeFileUrl(projectADM, settings) + // val fileUrl: String = makeFileUrl(projectADM, appConfig) targetSchema match { case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) @@ -3208,6 +3213,7 @@ case class StillImageFileValueContentV2( OntologyConstants.KnoraApiV2Complex.StillImageFileValueHasIIIFBaseUrl -> JsonLDUtil .datatypeValueToJsonLDObject( value = s"${settings.externalSipiIIIFGetUrl}/${projectADM.shortcode}", + // value = s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}", datatype = OntologyConstants.Xsd.Uri.toSmartIri ) ) 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 ccfc3cc834..fd737dd525 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 @@ -1509,7 +1509,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) val (maybeStandoffMinStartIndex: Option[Int], maybeStandoffMaxStartIndex: Option[Int]) = StandoffTagUtilV2.getStandoffMinAndMaxStartIndexesForTextValueQuery( queryStandoff = queryStandoff, - settings = settings + appConfig = appConfig ) for { @@ -1615,7 +1615,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, requestingUser = requestingUser ) @@ -1706,7 +1706,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, requestingUser = requestingUser ) @@ -1796,7 +1796,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) } gravsearchUrl: String = - s"${settings.internalSipiBaseUrl}/${resource.projectADM.shortcode}/${gravsearchFileValueContent.fileValue.internalFilename}/file" + s"${appConfig.sipi.internalBaseUrl}/${resource.projectADM.shortcode}/${gravsearchFileValueContent.fileValue.internalFilename}/file" } yield gravsearchUrl val recoveredGravsearchUrlFuture = gravsearchUrlFuture.recover { case notFound: NotFoundException => @@ -2179,7 +2179,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) startNodeOnly = false, maybeExcludeLinkProperty = excludePropertyInternal, outbound = outbound, // true to query outbound edges, false to query inbound edges - limit = settings.maxGraphBreadth + limit = appConfig.v2.graphRoute.maxGraphBreadth ) .toString() ) @@ -2320,7 +2320,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) maybeExcludeLinkProperty = excludePropertyInternal, startNodeOnly = true, outbound = true, - limit = settings.maxGraphBreadth + limit = appConfig.v2.graphRoute.maxGraphBreadth ) .toString() ) @@ -2610,7 +2610,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) Seq( JsonLDObject( Map( - "id" -> JsonLDString(settings.externalSipiIIIFGetUrl), + "id" -> JsonLDString(appConfig.sipi.externalBaseUrl), "type" -> JsonLDString("ImageService3"), "profile" -> JsonLDString("level1") ) 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 b4b7ade296..2aacafc728 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 @@ -363,7 +363,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = true, versionDate = None, appActor = appActor, - settings = settings, + appConfig = appConfig, targetSchema = targetSchema, requestingUser = requestingUser ) @@ -725,7 +725,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) versionDate = None, calculateMayHaveMoreResults = true, appActor = appActor, - settings = settings, + appConfig = appConfig, targetSchema = targetSchema, requestingUser = requestingUser ) @@ -855,7 +855,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) StandoffTagUtilV2 .getStandoffMinAndMaxStartIndexesForTextValueQuery( queryStandoff = queryStandoff, - settings = settings + appConfig = appConfig ) // Are there any matching resources? @@ -921,7 +921,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = true, appActor = appActor, targetSchema = resourcesInProjectGetRequestV2.targetSchema, - settings = settings, + appConfig = appConfig, requestingUser = resourcesInProjectGetRequestV2.requestingUser ) } yield readResourcesSequence @@ -1068,7 +1068,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = true, appActor = appActor, targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, requestingUser = requestingUser ) 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 57cbda95dd..13d21b8c8c 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 @@ -156,7 +156,7 @@ class StandoffResponderV2(responderData: ResponderData, appConfig: AppConfig) ex versionDate = None, appActor = appActor, targetSchema = getStandoffRequestV2.targetSchema, - settings = settings, + appConfig = appConfig, requestingUser = getStandoffRequestV2.requestingUser ) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2Spec.scala index 88589d6a59..a04b28b137 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2Spec.scala @@ -67,7 +67,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = incunabulaUser ) @@ -108,7 +108,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = anythingAdminUser ) @@ -150,7 +150,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = incunabulaUser ) @@ -192,7 +192,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = anythingAdminUser ) @@ -234,7 +234,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = anonymousUser ) @@ -276,7 +276,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = anythingAdminUser ) @@ -348,7 +348,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = incunabulaUser ) @@ -421,7 +421,7 @@ class ConstructResponseUtilV2Spec extends CoreSpec() with ImplicitSender { calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = ApiV2Complex, - settings = settings, + appConfig = appConfig, requestingUser = incunabulaUser ) From 43a004376041fe28e421ee4e41b320222f4143d5 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 17:26:01 +0200 Subject: [PATCH 11/44] remove unnecessary brackets --- .../dsp/project/listener/external/ProjectListenerExternal.scala | 2 +- .../dsp/project/listener/internal/ProjectListenerInternal.scala | 2 +- .../src/main/scala/dsp/project/route/ProjectRoute.scala | 2 +- .../project/listener/external/ProjectListenerExternalSpec.scala | 2 +- .../project/listener/internal/ProjectListenerInternalSpec.scala | 2 +- .../src/test/scala/dsp/project/route/ProjectRouteSpec.scala | 2 +- dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala | 2 +- .../scala/dsp/role/listener/external/RoleListenerExternal.scala | 2 +- .../scala/dsp/role/listener/internal/RoleListenerInternal.scala | 2 +- .../interface/src/main/scala/dsp/role/route/RoleRoute.scala | 2 +- .../dsp/role/listener/external/RoleListenerExternalSpec.scala | 2 +- .../dsp/role/listener/internal/RoleListenerInternalSpec.scala | 2 +- .../interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala | 2 +- dsp-user/core/src/test/scala/dsp/user/api/UserApiSpec.scala | 2 +- .../scala/dsp/user/listener/external/UserListenerExternal.scala | 2 +- .../scala/dsp/user/listener/internal/UserListenerInternal.scala | 2 +- .../interface/src/main/scala/dsp/user/route/UserRoute.scala | 2 +- .../dsp/user/listener/external/UserListenerExternalSpec.scala | 2 +- .../dsp/user/listener/internal/UserListenerInternalSpec.scala | 2 +- .../interface/src/test/scala/dsp/user/route/UserRouteSpec.scala | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala b/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala index 8d3e981446..157ae36e49 100644 --- a/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala +++ b/dsp-project/interface/src/main/scala/dsp/project/listener/external/ProjectListenerExternal.scala @@ -5,4 +5,4 @@ package dsp.project.listener.external -object placeholder {} +object placeholder diff --git a/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala b/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala index 20a476fe06..398153be93 100644 --- a/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala +++ b/dsp-project/interface/src/main/scala/dsp/project/listener/internal/ProjectListenerInternal.scala @@ -5,4 +5,4 @@ package dsp.project.listener.internal -object placeholder {} +object placeholder diff --git a/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala b/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala index 51f5cd5171..2d623e467d 100644 --- a/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala +++ b/dsp-project/interface/src/main/scala/dsp/project/route/ProjectRoute.scala @@ -5,4 +5,4 @@ package dsp.project.route -object placeholder {} +object placeholder diff --git a/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala b/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala index 8d3e981446..157ae36e49 100644 --- a/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala +++ b/dsp-project/interface/src/test/scala/dsp/project/listener/external/ProjectListenerExternalSpec.scala @@ -5,4 +5,4 @@ package dsp.project.listener.external -object placeholder {} +object placeholder diff --git a/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala b/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala index 20a476fe06..398153be93 100644 --- a/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala +++ b/dsp-project/interface/src/test/scala/dsp/project/listener/internal/ProjectListenerInternalSpec.scala @@ -5,4 +5,4 @@ package dsp.project.listener.internal -object placeholder {} +object placeholder diff --git a/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala b/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala index 51f5cd5171..2d623e467d 100644 --- a/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala +++ b/dsp-project/interface/src/test/scala/dsp/project/route/ProjectRouteSpec.scala @@ -5,4 +5,4 @@ package dsp.project.route -object placeholder {} +object placeholder diff --git a/dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala b/dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala index 4f52163360..f0ac5ab9c7 100644 --- a/dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala +++ b/dsp-role/core/src/test/scala/dsp/role/api/RoleApiSpec.scala @@ -8,4 +8,4 @@ package dsp.role.api /** * To be implemented... */ -object placeholder {} +object placeholder diff --git a/dsp-role/interface/src/main/scala/dsp/role/listener/external/RoleListenerExternal.scala b/dsp-role/interface/src/main/scala/dsp/role/listener/external/RoleListenerExternal.scala index 034906962c..3525ba5667 100644 --- a/dsp-role/interface/src/main/scala/dsp/role/listener/external/RoleListenerExternal.scala +++ b/dsp-role/interface/src/main/scala/dsp/role/listener/external/RoleListenerExternal.scala @@ -8,4 +8,4 @@ package dsp.role.listener.external /** * To be implemented... */ -object placeholder {} +object placeholder diff --git a/dsp-role/interface/src/main/scala/dsp/role/listener/internal/RoleListenerInternal.scala b/dsp-role/interface/src/main/scala/dsp/role/listener/internal/RoleListenerInternal.scala index 7cae25efca..31e05a1fd0 100644 --- a/dsp-role/interface/src/main/scala/dsp/role/listener/internal/RoleListenerInternal.scala +++ b/dsp-role/interface/src/main/scala/dsp/role/listener/internal/RoleListenerInternal.scala @@ -8,4 +8,4 @@ package dsp.role.listener.internal /** * To be implemented... */ -object placeholder {} +object placeholder diff --git a/dsp-role/interface/src/main/scala/dsp/role/route/RoleRoute.scala b/dsp-role/interface/src/main/scala/dsp/role/route/RoleRoute.scala index 66c2294cc2..d0c8f759ec 100644 --- a/dsp-role/interface/src/main/scala/dsp/role/route/RoleRoute.scala +++ b/dsp-role/interface/src/main/scala/dsp/role/route/RoleRoute.scala @@ -8,4 +8,4 @@ package dsp.role.route /** * To be implemented... */ -object placeholder {} +object placeholder diff --git a/dsp-role/interface/src/test/scala/dsp/role/listener/external/RoleListenerExternalSpec.scala b/dsp-role/interface/src/test/scala/dsp/role/listener/external/RoleListenerExternalSpec.scala index 034906962c..3525ba5667 100644 --- a/dsp-role/interface/src/test/scala/dsp/role/listener/external/RoleListenerExternalSpec.scala +++ b/dsp-role/interface/src/test/scala/dsp/role/listener/external/RoleListenerExternalSpec.scala @@ -8,4 +8,4 @@ package dsp.role.listener.external /** * To be implemented... */ -object placeholder {} +object placeholder diff --git a/dsp-role/interface/src/test/scala/dsp/role/listener/internal/RoleListenerInternalSpec.scala b/dsp-role/interface/src/test/scala/dsp/role/listener/internal/RoleListenerInternalSpec.scala index 7cae25efca..31e05a1fd0 100644 --- a/dsp-role/interface/src/test/scala/dsp/role/listener/internal/RoleListenerInternalSpec.scala +++ b/dsp-role/interface/src/test/scala/dsp/role/listener/internal/RoleListenerInternalSpec.scala @@ -8,4 +8,4 @@ package dsp.role.listener.internal /** * To be implemented... */ -object placeholder {} +object placeholder diff --git a/dsp-role/interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala b/dsp-role/interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala index 66c2294cc2..d0c8f759ec 100644 --- a/dsp-role/interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala +++ b/dsp-role/interface/src/test/scala/dsp/role/route/RoleRouteSpec.scala @@ -8,4 +8,4 @@ package dsp.role.route /** * To be implemented... */ -object placeholder {} +object placeholder diff --git a/dsp-user/core/src/test/scala/dsp/user/api/UserApiSpec.scala b/dsp-user/core/src/test/scala/dsp/user/api/UserApiSpec.scala index 752c5a7093..742aa581ba 100644 --- a/dsp-user/core/src/test/scala/dsp/user/api/UserApiSpec.scala +++ b/dsp-user/core/src/test/scala/dsp/user/api/UserApiSpec.scala @@ -7,4 +7,4 @@ package dsp.user.api // this placeholder can be removed as soon as the code are there // without it there is a warning -object placeholder {} +object placeholder diff --git a/dsp-user/interface/src/main/scala/dsp/user/listener/external/UserListenerExternal.scala b/dsp-user/interface/src/main/scala/dsp/user/listener/external/UserListenerExternal.scala index 707ab682b1..e705fb991c 100644 --- a/dsp-user/interface/src/main/scala/dsp/user/listener/external/UserListenerExternal.scala +++ b/dsp-user/interface/src/main/scala/dsp/user/listener/external/UserListenerExternal.scala @@ -7,4 +7,4 @@ package dsp.user.listener.external // this placeholder can be removed as soon as the code are there // without it there is a warning -object placeholder {} +object placeholder diff --git a/dsp-user/interface/src/main/scala/dsp/user/listener/internal/UserListenerInternal.scala b/dsp-user/interface/src/main/scala/dsp/user/listener/internal/UserListenerInternal.scala index 6459b362ab..8c5e323b95 100644 --- a/dsp-user/interface/src/main/scala/dsp/user/listener/internal/UserListenerInternal.scala +++ b/dsp-user/interface/src/main/scala/dsp/user/listener/internal/UserListenerInternal.scala @@ -7,4 +7,4 @@ package dsp.user.listener.internal // this placeholder can be removed as soon as the code are there // without it there is a warning -object placeholder {} +object placeholder diff --git a/dsp-user/interface/src/main/scala/dsp/user/route/UserRoute.scala b/dsp-user/interface/src/main/scala/dsp/user/route/UserRoute.scala index 3db4a717f2..85db86dc14 100644 --- a/dsp-user/interface/src/main/scala/dsp/user/route/UserRoute.scala +++ b/dsp-user/interface/src/main/scala/dsp/user/route/UserRoute.scala @@ -7,4 +7,4 @@ package dsp.user.route // this placeholder can be removed as soon as the code are there // without it there is a warning -object placeholder {} +object placeholder diff --git a/dsp-user/interface/src/test/scala/dsp/user/listener/external/UserListenerExternalSpec.scala b/dsp-user/interface/src/test/scala/dsp/user/listener/external/UserListenerExternalSpec.scala index 707ab682b1..e705fb991c 100644 --- a/dsp-user/interface/src/test/scala/dsp/user/listener/external/UserListenerExternalSpec.scala +++ b/dsp-user/interface/src/test/scala/dsp/user/listener/external/UserListenerExternalSpec.scala @@ -7,4 +7,4 @@ package dsp.user.listener.external // this placeholder can be removed as soon as the code are there // without it there is a warning -object placeholder {} +object placeholder diff --git a/dsp-user/interface/src/test/scala/dsp/user/listener/internal/UserListenerInternalSpec.scala b/dsp-user/interface/src/test/scala/dsp/user/listener/internal/UserListenerInternalSpec.scala index 6459b362ab..8c5e323b95 100644 --- a/dsp-user/interface/src/test/scala/dsp/user/listener/internal/UserListenerInternalSpec.scala +++ b/dsp-user/interface/src/test/scala/dsp/user/listener/internal/UserListenerInternalSpec.scala @@ -7,4 +7,4 @@ package dsp.user.listener.internal // this placeholder can be removed as soon as the code are there // without it there is a warning -object placeholder {} +object placeholder diff --git a/dsp-user/interface/src/test/scala/dsp/user/route/UserRouteSpec.scala b/dsp-user/interface/src/test/scala/dsp/user/route/UserRouteSpec.scala index 3db4a717f2..85db86dc14 100644 --- a/dsp-user/interface/src/test/scala/dsp/user/route/UserRouteSpec.scala +++ b/dsp-user/interface/src/test/scala/dsp/user/route/UserRouteSpec.scala @@ -7,4 +7,4 @@ package dsp.user.route // this placeholder can be removed as soon as the code are there // without it there is a warning -object placeholder {} +object placeholder From 44f7a71df9f6eb20435c00769ea99f43693f64ac Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 17:38:03 +0200 Subject: [PATCH 12/44] remove unused code --- .../scala/org/knora/webapi/routing/RouteUtilV2.scala | 11 ----------- .../knora/webapi/routing/v2/ResourcesRouteV2.scala | 1 - 2 files changed, 12 deletions(-) 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 e4ac441854..2e404c3dc4 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala @@ -220,10 +220,6 @@ object RouteUtilV2 { targetSchema: OntologySchema, schemaOptions: Set[SchemaOption] )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[RouteResult] = { - // Optionally log the request message. TODO: move this to the testing framework. - if (settings.dumpMessages) { - log.debug(requestMessage.toString) - } val httpResponse: Future[HttpResponse] = for { // Make sure the responder sent a reply of type KnoraResponseV2. @@ -238,11 +234,6 @@ object RouteUtilV2 { ) } - // Optionally log the reply message. TODO: move this to the testing framework. - _ = if (settings.dumpMessages) { - log.debug(knoraResponse.toString) - } - // Choose a media type for the response. responseMediaType: MediaType.NonBinary = chooseRdfMediaTypeForResponse(requestContext) @@ -275,7 +266,6 @@ object RouteUtilV2 { * * @param requestMessageF a future containing a [[KnoraRequestV2]] message that should be sent to the responder manager. * @param requestContext the akka-http [[RequestContext]]. - * @param settings the application's settings. * @param responderManager a reference to the responder manager. * @param log a logging adapter. * @param targetSchema the API schema that should be used in the response. @@ -286,7 +276,6 @@ object RouteUtilV2 { def runTEIXMLRoute( requestMessageF: Future[KnoraRequestV2], requestContext: RequestContext, - settings: KnoraSettingsImpl, appActor: ActorRef, log: Logger, targetSchema: ApiV2Schema diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 978cafd58d..e7753dc6d0 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -438,7 +438,6 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runTEIXMLRoute( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext) From e032cd9124504247c4149e2cc2a67a77326b8106 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 12 Sep 2022 18:00:35 +0200 Subject: [PATCH 13/44] add appConfig to Responder --- .../webapi/core/actors/RoutingActor.scala | 20 +++++++++---------- .../knora/webapi/responders/Responder.scala | 8 +++++--- .../responders/admin/GroupsResponderADM.scala | 5 ++++- .../responders/admin/ListsResponderADM.scala | 3 ++- .../admin/PermissionsResponderADM.scala | 4 +++- .../admin/ProjectsResponderADM.scala | 5 ++++- .../responders/admin/SipiResponderADM.scala | 3 ++- .../responders/admin/StoresResponderADM.scala | 3 ++- .../responders/admin/UsersResponderADM.scala | 2 +- .../responders/v1/CkanResponderV1.scala | 3 ++- .../responders/v1/ListsResponderV1.scala | 2 +- .../responders/v1/OntologyResponderV1.scala | 3 ++- .../responders/v1/ProjectsResponderV1.scala | 4 +++- .../responders/v1/ResourcesResponderV1.scala | 3 ++- .../responders/v1/SearchResponderV1.scala | 3 ++- .../responders/v1/StandoffResponderV1.scala | 4 +++- .../responders/v1/UsersResponderV1.scala | 2 +- .../responders/v1/ValuesResponderV1.scala | 3 ++- .../responders/v2/ListsResponderV2.scala | 2 +- .../responders/v2/OntologyResponderV2.scala | 3 ++- .../responders/v2/ResourcesResponderV2.scala | 2 +- .../v2/ResponderWithStandoffV2.scala | 4 +++- .../responders/v2/SearchResponderV2.scala | 2 +- .../responders/v2/StandoffResponderV2.scala | 3 ++- .../responders/v2/ValuesResponderV2.scala | 4 +++- .../admin/PermissionsResponderADMSpec.scala | 2 +- 26 files changed, 64 insertions(+), 38 deletions(-) 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 78e8557bc1..69f2c28780 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 @@ -81,8 +81,6 @@ class RoutingActor( implicit val system: ActorSystem = context.system val log: Logger = Logger(this.getClass()) - log.debug("entered the ApplicationActor constructor") - /** * The application's configuration. */ @@ -101,7 +99,7 @@ class RoutingActor( /** * Timeout definition */ - implicit protected val timeout: Timeout = knoraSettings.defaultTimeout + implicit protected val timeout: Timeout = appConfig.defaultTimeoutAsDuration /** * Data used in responders. @@ -109,32 +107,32 @@ class RoutingActor( val responderData: ResponderData = ResponderData(system, self, knoraSettings, cacheServiceSettings) // V1 responders - val ckanResponderV1: CkanResponderV1 = new CkanResponderV1(responderData) + val ckanResponderV1: CkanResponderV1 = new CkanResponderV1(responderData, appConfig) val resourcesResponderV1: ResourcesResponderV1 = new ResourcesResponderV1(responderData, appConfig) val valuesResponderV1: ValuesResponderV1 = new ValuesResponderV1(responderData, appConfig) - val standoffResponderV1: StandoffResponderV1 = new StandoffResponderV1(responderData) + val standoffResponderV1: StandoffResponderV1 = new StandoffResponderV1(responderData, appConfig) val usersResponderV1: UsersResponderV1 = new UsersResponderV1(responderData, appConfig) val listsResponderV1: ListsResponderV1 = new ListsResponderV1(responderData, appConfig) val searchResponderV1: SearchResponderV1 = new SearchResponderV1(responderData, appConfig) val ontologyResponderV1: OntologyResponderV1 = new OntologyResponderV1(responderData, appConfig) - val projectsResponderV1: ProjectsResponderV1 = new ProjectsResponderV1(responderData) + val projectsResponderV1: ProjectsResponderV1 = new ProjectsResponderV1(responderData, appConfig) // V2 responders val ontologiesResponderV2: OntologyResponderV2 = new OntologyResponderV2(responderData, appConfig) val searchResponderV2: SearchResponderV2 = new SearchResponderV2(responderData, appConfig) val resourcesResponderV2: ResourcesResponderV2 = new ResourcesResponderV2(responderData, appConfig) - val valuesResponderV2: ValuesResponderV2 = new ValuesResponderV2(responderData) + val valuesResponderV2: ValuesResponderV2 = new ValuesResponderV2(responderData, appConfig) val standoffResponderV2: StandoffResponderV2 = new StandoffResponderV2(responderData, appConfig) val listsResponderV2: ListsResponderV2 = new ListsResponderV2(responderData, appConfig) // Admin responders - val groupsResponderADM: GroupsResponderADM = new GroupsResponderADM(responderData) + val groupsResponderADM: GroupsResponderADM = new GroupsResponderADM(responderData, appConfig) val listsResponderADM: ListsResponderADM = new ListsResponderADM(responderData, appConfig) - val permissionsResponderADM: PermissionsResponderADM = new PermissionsResponderADM(responderData) - val projectsResponderADM: ProjectsResponderADM = new ProjectsResponderADM(responderData) + val permissionsResponderADM: PermissionsResponderADM = new PermissionsResponderADM(responderData, appConfig) + val projectsResponderADM: ProjectsResponderADM = new ProjectsResponderADM(responderData, appConfig) val storeResponderADM: StoresResponderADM = new StoresResponderADM(responderData, appConfig) val usersResponderADM: UsersResponderADM = new UsersResponderADM(responderData, appConfig) - val sipiRouterADM: SipiResponderADM = new SipiResponderADM(responderData) + val sipiRouterADM: SipiResponderADM = new SipiResponderADM(responderData, appConfig) def receive: Receive = { 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 5cd8f5652d..e283de37c7 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -18,13 +18,15 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import dsp.errors._ +import org.knora.webapi.config.AppConfig +import org.knora.webapi.settings.KnoraDispatchers +import org.knora.webapi.settings.KnoraSettingsImpl 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 settings.{KnoraDispatchers, KnoraSettingsImpl} /** * Responder helper methods. @@ -49,7 +51,7 @@ object Responder { /** * An abstract class providing values that are commonly used in Knora responders. */ -abstract class Responder(responderData: ResponderData) extends LazyLogging { +abstract class Responder(responderData: ResponderData, appConfig: AppConfig) extends LazyLogging { /** * The actor system. @@ -85,7 +87,7 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging { /** * The application's default timeout for `ask` messages. */ - protected implicit val timeout: Timeout = settings.defaultTimeout + protected implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration /** * Provides logging 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 6bc3d99b45..ecee4e5b4b 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 @@ -14,6 +14,7 @@ import scala.concurrent.Future import dsp.errors._ import dsp.valueobjects.Group.GroupStatus import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -33,7 +34,9 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Returns information about Knora projects. */ -class GroupsResponderADM(responderData: ResponderData) extends Responder(responderData) with GroupsADMJsonProtocol { +class GroupsResponderADM(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) + with GroupsADMJsonProtocol { // Global lock IRI used for group creation and updating private val GROUPS_GLOBAL_LOCK_IRI: IRI = "http://rdfh.ch/groups" 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 01dd83f0c7..e512420691 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 @@ -39,7 +39,8 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * A responder that returns information about hierarchical lists. */ -class ListsResponderADM(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { +class ListsResponderADM(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { // The IRI used to lock user creation and update private val LISTS_GLOBAL_LOCK_IRI = "http://rdfh.ch/lists" 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 674b47fe61..80d109fcad 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 @@ -15,6 +15,7 @@ import scala.concurrent.Future import dsp.errors._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -40,7 +41,8 @@ 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, appConfig: AppConfig) + extends Responder(responderData, appConfig) { private val PERMISSIONS_GLOBAL_LOCK_IRI = "http://rdfh.ch/permissions" /* Entity types used to more clearly distinguish what kind of entity is meant */ 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 2da00de35d..39dc9ef1f4 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 @@ -21,6 +21,7 @@ import scala.util.Try import dsp.errors._ import org.knora.webapi._ import org.knora.webapi.annotation.ApiMayChange +import org.knora.webapi.config.AppConfig import org.knora.webapi.instrumentation.InstrumentationSupport import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants @@ -47,7 +48,9 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Returns information about Knora projects. */ -class ProjectsResponderADM(responderData: ResponderData) extends Responder(responderData) with InstrumentationSupport { +class ProjectsResponderADM(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) + with InstrumentationSupport { // Global lock IRI used for project creation and update private val PROJECTS_GLOBAL_LOCK_IRI = "http://rdfh.ch/projects" 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 c4169db62e..40e8a19e51 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 @@ -12,6 +12,7 @@ import scala.concurrent.Future import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectRestrictedViewSettingsADM @@ -34,7 +35,7 @@ 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, appConfig: AppConfig) extends Responder(responderData, appConfig) { /** * 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 b643b9232b..7951764dde 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 @@ -30,7 +30,8 @@ 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, appConfig: AppConfig) extends Responder(responderData) { +class StoresResponderADM(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { /** * 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 bef127e117..64e6cfd6c7 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 @@ -46,7 +46,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage * Provides information about Knora users to other responders. */ class UsersResponderADM(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData) + extends Responder(responderData, appConfig) with InstrumentationSupport { // The IRI used to lock user creation and update 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 8bfcd67de4..9555cca379 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 @@ -15,6 +15,7 @@ import scala.concurrent.Future import scala.concurrent.duration._ import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages.SparqlSelectRequest @@ -40,7 +41,7 @@ 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, appConfig: AppConfig) extends Responder(responderData, appConfig) { /** * 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 533f03864b..e00aace2dc 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 @@ -25,7 +25,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * A responder that returns information about hierarchical lists. */ -class ListsResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { +class ListsResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData, appConfig) { /** * 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 107b4d2207..bcf4058072 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 @@ -36,7 +36,8 @@ 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, appConfig: AppConfig) extends Responder(responderData) { +class OntologyResponderV1(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { private val valueUtilV1 = new ValueUtilV1(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 e4a1650f1f..c6101f0f1e 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 @@ -13,6 +13,7 @@ import scala.concurrent.Future import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.admin.responder.usersmessages.UserGetRequestADM @@ -34,7 +35,8 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Returns information about Knora projects. */ -class ProjectsResponderV1(responderData: ResponderData) extends Responder(responderData) { +class ProjectsResponderV1(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { // 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 1c3216de2e..2cd872d672 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 @@ -59,7 +59,8 @@ 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, appConfig: AppConfig) extends Responder(responderData) { +class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { // Converts SPARQL query results to ApiValueV1 objects. private val valueUtilV1 = new ValueUtilV1(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 1b0863502c..a082d91c14 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 @@ -36,7 +36,8 @@ 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, appConfig: AppConfig) extends Responder(responderData) { +class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { // 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 d73a7eb54e..59868892e5 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 @@ -12,6 +12,7 @@ import scala.concurrent.Future import dsp.errors.NotFoundException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -27,7 +28,8 @@ 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, appConfig: AppConfig) + extends Responder(responderData, appConfig) { /** * 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 f4d716aa19..e4c2e55666 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 @@ -35,7 +35,7 @@ import org.knora.webapi.util.cache.CacheUtil /** * Provides information about Knora users to other responders. */ -class UsersResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { +class UsersResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData, appConfig) { // 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 1e664aeee3..0b7e8226f8 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 @@ -55,7 +55,8 @@ import org.knora.webapi.util._ /** * Updates Knora values. */ -class ValuesResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { +class ValuesResponderV1(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { // Converts SPARQL query results to ApiValueV1 objects. val valueUtilV1 = new ValueUtilV1(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 e2d3e62096..f70872f1bf 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 @@ -24,7 +24,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Responds to requests relating to lists and nodes. */ -class ListsResponderV2(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData) { +class ListsResponderV2(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData, appConfig) { /** * 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 a481234545..0f7a151c0d 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 @@ -61,7 +61,8 @@ 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, appConfig: AppConfig) extends Responder(responderData) { +class OntologyResponderV2(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { /** * Receives a message of type [[OntologiesResponderRequestV2]], and returns an appropriate response message. 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 fd737dd525..ba0dd27180 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 @@ -66,7 +66,7 @@ import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util._ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) - extends ResponderWithStandoffV2(responderData) { + extends ResponderWithStandoffV2(responderData, appConfig) { /* actor materializer needed for http requests */ implicit val materializer: Materializer = Materializer.matFromSystem(system) 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 670ae124f2..44d0c121d1 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 @@ -12,6 +12,7 @@ import scala.concurrent.Future import dsp.errors.NotFoundException import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.ConstructResponseUtilV2 import org.knora.webapi.messages.util.ConstructResponseUtilV2.MappingAndXSLTransformation @@ -27,7 +28,8 @@ 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, appConfig: AppConfig) + extends Responder(responderData, appConfig) { /** * 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 2aacafc728..91a7282634 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 @@ -51,7 +51,7 @@ import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException import org.knora.webapi.util.ApacheLuceneSupport._ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) - extends ResponderWithStandoffV2(responderData) { + extends ResponderWithStandoffV2(responderData, appConfig) { // A Gravsearch type inspection runner. private val gravsearchTypeInspectionRunner = new GravsearchTypeInspectionRunner(appActor, responderData) 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 13d21b8c8c..beaae477ad 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 @@ -63,7 +63,8 @@ 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, appConfig: AppConfig) extends Responder(responderData) { +class StandoffResponderV2(responderData: ResponderData, appConfig: AppConfig) + extends Responder(responderData, appConfig) { /* actor materializer needed for http requests */ implicit val materializer: Materializer = Materializer.matFromSystem(system) 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 46bca3e06e..3d0fb542fc 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 @@ -15,6 +15,7 @@ import scala.concurrent.Future import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -43,7 +44,8 @@ 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, appConfig: AppConfig) + extends Responder(responderData, appConfig) { /** * The IRI and content of a new value or value version whose existence in the triplestore has been verified. diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala index f3343cc0f2..a762f5aa6c 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala @@ -145,7 +145,7 @@ class PermissionsResponderADMSpec extends CoreSpec with ImplicitSender with Priv "ask for userAdministrativePermissionsGetADM" should { "return user's administrative permissions (helper method used in queries before)" in { - val permissionsResponder = new PermissionsResponderADM(responderData) + val permissionsResponder = new PermissionsResponderADM(responderData, appConfig) val f: Future[Map[IRI, Set[PermissionADM]]] = permissionsResponder.userAdministrativePermissionsGetADM( From 09c83463348f9108b9769d612d4050ac59d4a08d Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Tue, 13 Sep 2022 17:55:18 +0200 Subject: [PATCH 14/44] wip: all but one test fails --- .../org/knora/webapi/config/AppConfig.scala | 11 +- .../webapi/core/actors/RoutingActor.scala | 9 +- .../http/directives/DSPApiDirectives.scala | 8 +- .../http/handler/KnoraExceptionHandler.scala | 42 ++-- .../webapi/messages/StringFormatter.scala | 7 +- .../webapi/messages/util/ResponderData.scala | 3 - .../messages/util/rdf/RdfFeatureFactory.scala | 4 +- .../search/FullTextMainQueryGenerator.scala | 7 +- .../GravsearchMainQueryGenerator.scala | 7 +- ...cificGravsearchToPrequeryTransformer.scala | 10 +- .../v2/responder/KnoraRequestV2.scala | 3 - .../v2/responder/KnoraResponseV2.scala | 18 +- .../listsmessages/ListsMessagesV2.scala | 6 +- .../ontologymessages/OntologyMessagesV2.scala | 59 ++---- .../resourcemessages/ResourceMessagesV2.scala | 51 ++--- .../searchmessages/SearchMessagesV2.scala | 4 +- .../standoffmessages/StandoffMessagesV2.scala | 7 +- .../valuemessages/ValueMessagesV2.scala | 191 +++++------------- .../knora/webapi/responders/Responder.scala | 6 - .../responders/v2/ResourcesResponderV2.scala | 6 +- .../responders/v2/SearchResponderV2.scala | 14 +- .../org/knora/webapi/routing/ApiRoutes.scala | 75 +++---- .../knora/webapi/routing/Authenticator.scala | 137 +++++++------ .../knora/webapi/routing/HealthRoute.scala | 10 +- .../org/knora/webapi/routing/KnoraRoute.scala | 14 +- .../knora/webapi/routing/RejectingRoute.scala | 7 +- .../knora/webapi/routing/RouteUtilADM.scala | 13 -- .../knora/webapi/routing/RouteUtilV1.scala | 34 +--- .../knora/webapi/routing/RouteUtilV2.scala | 19 +- .../webapi/routing/SwaggerApiDocsRoute.scala | 60 ------ .../webapi/routing/admin/FilesRouteADM.scala | 8 +- .../webapi/routing/admin/GroupsRouteADM.scala | 26 +-- .../webapi/routing/admin/ListsRouteADM.scala | 11 +- .../routing/admin/PermissionsRouteADM.scala | 11 +- .../routing/admin/ProjectsRouteADM.scala | 80 ++++---- .../webapi/routing/admin/StoreRouteADM.scala | 6 +- .../webapi/routing/admin/UsersRouteADM.scala | 80 ++++---- .../admin/lists/CreateListItemsRouteADM.scala | 11 +- .../admin/lists/DeleteListItemsRouteADM.scala | 14 +- .../admin/lists/GetListItemsRouteADM.scala | 19 +- .../admin/lists/UpdateListItemsRouteADM.scala | 20 +- .../CreatePermissionRouteADM.scala | 12 +- .../DeletePermissionRouteADM.scala | 9 +- .../permissions/GetPermissionsRouteADM.scala | 18 +- .../UpdatePermissionRouteADM.scala | 18 +- .../webapi/routing/v1/AssetsRouteV1.scala | 5 +- .../routing/v1/AuthenticationRouteV1.scala | 17 +- .../knora/webapi/routing/v1/CkanRouteV1.scala | 8 +- .../webapi/routing/v1/ListsRouteV1.scala | 11 +- .../webapi/routing/v1/ProjectsRouteV1.scala | 13 +- .../routing/v1/ResourceTypesRouteV1.scala | 23 +-- .../webapi/routing/v1/ResourcesRouteV1.scala | 52 ++--- .../webapi/routing/v1/SearchRouteV1.scala | 11 +- .../webapi/routing/v1/StandoffRouteV1.scala | 8 +- .../webapi/routing/v1/UsersRouteV1.scala | 22 +- .../webapi/routing/v1/ValuesRouteV1.scala | 41 ++-- .../routing/v2/AuthenticationRouteV2.scala | 18 +- .../webapi/routing/v2/ListsRouteV2.scala | 15 +- .../webapi/routing/v2/OntologiesRouteV2.scala | 158 ++++++++------- .../webapi/routing/v2/ResourcesRouteV2.scala | 155 ++++++++++---- .../webapi/routing/v2/SearchRouteV2.scala | 53 ++--- .../webapi/routing/v2/StandoffRouteV2.scala | 16 +- .../webapi/routing/v2/ValuesRouteV2.scala | 28 +-- .../store/iiif/impl/IIIFServiceSipiImpl.scala | 5 +- .../TriplestoreServiceHttpConnectorImpl.scala | 2 +- .../org/knora/webapi/util/FileUtil.scala | 25 +-- .../scala/org/knora/webapi/CoreSpec.scala | 12 +- .../test/scala/org/knora/webapi/E2ESpec.scala | 22 +- .../org/knora/webapi/ITKnoraLiveSpec.scala | 10 +- .../test/scala/org/knora/webapi/R2RSpec.scala | 17 +- .../knora/webapi/core/TestClientService.scala | 5 - .../knora/webapi/e2e/CORSSupportE2ESpec.scala | 2 +- .../webapi/e2e/ClientTestDataCollector.scala | 6 +- .../webapi/e2e/ExceptionHandlerR2RSpec.scala | 2 +- .../knora/webapi/e2e/HealthRouteE2ESpec.scala | 6 +- .../webapi/e2e/InstanceCheckerSpec.scala | 4 +- .../webapi/e2e/admin/FilesADME2ESpec.scala | 2 +- .../webapi/e2e/admin/GroupsADME2ESpec.scala | 2 +- .../e2e/admin/PermissionsADME2ESpec.scala | 2 +- .../webapi/e2e/admin/ProjectsADME2ESpec.scala | 2 +- .../webapi/e2e/admin/UsersADME2ESpec.scala | 2 +- .../CreateListItemsRouteADME2ESpec.scala | 2 +- .../DeleteListItemsRouteADME2ESpec.scala | 2 +- .../lists/GetListItemsRouteADME2ESpec.scala | 2 +- .../UpdateListItemsRouteADME2ESpec.scala | 2 +- .../e2e/http/ServerVersionE2ESpec.scala | 4 +- .../e2e/v1/AuthenticationV1E2ESpec.scala | 8 +- .../webapi/e2e/v1/ResourcesV1R2RSpec.scala | 13 +- .../knora/webapi/e2e/v1/SearchV1R2RSpec.scala | 6 +- .../knora/webapi/e2e/v1/SipiV1R2RSpec.scala | 14 +- .../webapi/e2e/v1/StandoffV1R2RSpec.scala | 10 +- .../knora/webapi/e2e/v1/ValuesV1R2RSpec.scala | 7 +- .../e2e/v2/AuthenticationV2E2ESpec.scala | 4 +- .../e2e/v2/JSONLDHandlingV2R2RSpec.scala | 6 +- .../webapi/e2e/v2/ListsRouteV2R2RSpec.scala | 8 +- .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 17 +- .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 160 +++++++-------- .../webapi/e2e/v2/SearchRouteV2R2RSpec.scala | 20 +- .../webapi/e2e/v2/ValuesRouteV2E2ESpec.scala | 2 +- .../knora/webapi/e2e/v2/ValuesV2R2RSpec.scala | 10 +- .../it/v1/KnoraSipiIntegrationV1ITSpec.scala | 2 +- .../it/v2/KnoraSipiAuthenticationITSpec.scala | 2 +- .../util/rdf/KnoraResponseV2Spec.scala | 15 +- ...cGravsearchToPrequeryTransformerSpec.scala | 60 +++--- .../admin/UsersResponderADMSpec.scala | 6 +- .../responders/v1/SearchResponderV1Spec.scala | 8 +- .../responders/v2/ResourceUtilV2Spec.scala | 2 +- .../responders/v2/ontology/CacheSpec.scala | 2 +- .../v2/ontology/CardinalitiesSpec.scala | 2 +- .../webapi/routing/AuthenticatorSpec.scala | 27 ++- .../knora/webapi/routing/JWTHelperSpec.scala | 46 ++--- 111 files changed, 1169 insertions(+), 1267 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 16c5257dc4..e4a4752d07 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -37,11 +37,16 @@ final case class AppConfig( shacl: Shacl, fallbackLanguage: String, maxResultsPerSearchResultPage: Int, - standoffPerPage: Int + standoffPerPage: Int, + routesToReject: List[String], + tmpDatadir: String, + clientTestDataService: ClientTestDataService + // collectClientTestData: Boolean ) { val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity) val defaultTimeoutAsDuration = scala.concurrent.duration.Duration.apply(defaultTimeout).asInstanceOf[duration.FiniteDuration] + } final case class KnoraAPI( @@ -180,6 +185,10 @@ final case class Shacl( val shapesDirPath = Paths.get(shapesDir) } +final case class ClientTestDataService( + collectClientTestData: Boolean +) + /** * Loads the applicaton configuration using ZIO-Config. ZIO-Config is capable of loading * the Typesafe-Config format. 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 69f2c28780..386b833f44 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 @@ -62,8 +62,6 @@ import org.knora.webapi.responders.v2.ResourcesResponderV2 import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.responders.v2.StandoffResponderV2 import org.knora.webapi.responders.v2.ValuesResponderV2 -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.store.cache.CacheServiceManager import org.knora.webapi.store.cache.settings.CacheServiceSettings import org.knora.webapi.store.iiif.IIIFServiceManager @@ -81,11 +79,6 @@ class RoutingActor( implicit val system: ActorSystem = context.system val log: Logger = Logger(this.getClass()) - /** - * The application's configuration. - */ - implicit val knoraSettings: KnoraSettingsImpl = KnoraSettings(system) - /** * The Cache Service's configuration. */ @@ -104,7 +97,7 @@ class RoutingActor( /** * Data used in responders. */ - val responderData: ResponderData = ResponderData(system, self, knoraSettings, cacheServiceSettings) + val responderData: ResponderData = ResponderData(system, self, cacheServiceSettings) // V1 responders val ckanResponderV1: CkanResponderV1 = new CkanResponderV1(responderData, appConfig) diff --git a/webapi/src/main/scala/org/knora/webapi/http/directives/DSPApiDirectives.scala b/webapi/src/main/scala/org/knora/webapi/http/directives/DSPApiDirectives.scala index 08a1319134..6f09929090 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/directives/DSPApiDirectives.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/directives/DSPApiDirectives.scala @@ -13,8 +13,8 @@ import akka.http.scaladsl.server.ExceptionHandler import akka.http.scaladsl.server.RejectionHandler import ch.megard.akka.http.cors.scaladsl.CorsDirectives +import org.knora.webapi.config.AppConfig import org.knora.webapi.http.handler.KnoraExceptionHandler -import org.knora.webapi.settings.KnoraSettings /** * DSP-API HTTP directives, used by wrapping around a routes, to influence @@ -26,9 +26,9 @@ object DSPApiDirectives { def rejectionHandler: RejectionHandler = CorsDirectives.corsRejectionHandler.withFallback(RejectionHandler.default) // Our exception handler - def exceptionHandler(system: ActorSystem): ExceptionHandler = KnoraExceptionHandler(KnoraSettings(system)) + def exceptionHandler(system: ActorSystem, appConfig: AppConfig): ExceptionHandler = KnoraExceptionHandler(appConfig) // Combining the two handlers for convenience - def handleErrors(system: ActorSystem): server.Directive[Unit] = - handleRejections(rejectionHandler) & handleExceptions(exceptionHandler(system)) + def handleErrors(system: ActorSystem, appConfig: AppConfig): server.Directive[Unit] = + handleRejections(rejectionHandler) & handleExceptions(exceptionHandler(system, appConfig)) } diff --git a/webapi/src/main/scala/org/knora/webapi/http/handler/KnoraExceptionHandler.scala b/webapi/src/main/scala/org/knora/webapi/http/handler/KnoraExceptionHandler.scala index 5a16c322b4..263973e68c 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/handler/KnoraExceptionHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/handler/KnoraExceptionHandler.scala @@ -17,13 +17,13 @@ import spray.json.JsValue import dsp.errors.InternalServerException import dsp.errors.RequestRejectedException +import org.knora.webapi.config.AppConfig import org.knora.webapi.http.status.ApiStatusCodesV1 import org.knora.webapi.http.status.ApiStatusCodesV2 import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.messages.util.rdf.JsonLDObject import org.knora.webapi.messages.util.rdf.JsonLDString -import org.knora.webapi.settings.KnoraSettingsImpl /** * The Knora exception handler is used by akka-http to convert any exceptions thrown during route processing @@ -35,7 +35,7 @@ object KnoraExceptionHandler extends LazyLogging { private val GENERIC_INTERNAL_SERVER_ERROR_MESSAGE = "The request could not be completed because of an internal server error." - def apply(settingsImpl: KnoraSettingsImpl): ExceptionHandler = ExceptionHandler { + def apply(appConfig: AppConfig): ExceptionHandler = ExceptionHandler { /* TODO: Find out which response format should be generated, by looking at what the client is requesting / accepting (issue #292) */ @@ -44,11 +44,11 @@ object KnoraExceptionHandler extends LazyLogging { val url = request.uri.path.toString if (url.startsWith("/v1")) { - complete(exceptionToJsonHttpResponseV1(rre, settingsImpl)) + complete(exceptionToJsonHttpResponseV1(rre, appConfig)) } else if (url.startsWith("/v2")) { - complete(exceptionToJsonHttpResponseV2(rre, settingsImpl)) + complete(exceptionToJsonHttpResponseV2(rre, appConfig)) } else { - complete(exceptionToJsonHttpResponseADM(rre, settingsImpl)) + complete(exceptionToJsonHttpResponseADM(rre, appConfig)) } } @@ -60,11 +60,11 @@ object KnoraExceptionHandler extends LazyLogging { logger.error(s"Internal Server Exception: Unable to run route $url", ise) if (url.startsWith("/v1")) { - complete(exceptionToJsonHttpResponseV1(ise, settingsImpl)) + complete(exceptionToJsonHttpResponseV1(ise, appConfig)) } else if (url.startsWith("/v2")) { - complete(exceptionToJsonHttpResponseV2(ise, settingsImpl)) + complete(exceptionToJsonHttpResponseV2(ise, appConfig)) } else { - complete(exceptionToJsonHttpResponseADM(ise, settingsImpl)) + complete(exceptionToJsonHttpResponseADM(ise, appConfig)) } } @@ -77,11 +77,11 @@ object KnoraExceptionHandler extends LazyLogging { logger.error(s"Unable to run route $url", other) if (url.startsWith("/v1")) { - complete(exceptionToJsonHttpResponseV1(other, settingsImpl)) + complete(exceptionToJsonHttpResponseV1(other, appConfig)) } else if (url.startsWith("/v2")) { - complete(exceptionToJsonHttpResponseV2(other, settingsImpl)) + complete(exceptionToJsonHttpResponseV2(other, appConfig)) } else { - complete(exceptionToJsonHttpResponseADM(other, settingsImpl)) + complete(exceptionToJsonHttpResponseADM(other, appConfig)) } } } @@ -92,7 +92,7 @@ object KnoraExceptionHandler extends LazyLogging { * @param ex the exception to be converted. * @return an [[HttpResponse]] in JSON format. */ - private def exceptionToJsonHttpResponseV1(ex: Throwable, settings: KnoraSettingsImpl): HttpResponse = { + private def exceptionToJsonHttpResponseV1(ex: Throwable, appConfig: AppConfig): HttpResponse = { // Get the API status code that corresponds to the exception. val apiStatus: ApiStatusCodesV1.Value = ApiStatusCodesV1.fromException(ex) @@ -110,7 +110,7 @@ object KnoraExceptionHandler extends LazyLogging { val responseFields: Map[String, JsValue] = Map( "status" -> JsNumber(apiStatus.id), - "error" -> JsString(makeClientErrorMessage(ex, settings)) + "error" -> JsString(makeClientErrorMessage(ex, appConfig)) ) ++ maybeAccess HttpResponse( @@ -125,7 +125,7 @@ object KnoraExceptionHandler extends LazyLogging { * @param ex the exception to be converted. * @return an [[HttpResponse]] in JSON format. */ - private def exceptionToJsonHttpResponseV2(ex: Throwable, settings: KnoraSettingsImpl): HttpResponse = { + private def exceptionToJsonHttpResponseV2(ex: Throwable, appConfig: AppConfig): HttpResponse = { // Get the HTTP status code that corresponds to the exception. val httpStatus: StatusCode = ApiStatusCodesV2.fromException(ex) @@ -133,7 +133,7 @@ object KnoraExceptionHandler extends LazyLogging { val jsonLDDocument = JsonLDDocument( body = JsonLDObject( - Map(OntologyConstants.KnoraApiV2Complex.Error -> JsonLDString(makeClientErrorMessage(ex, settings))) + Map(OntologyConstants.KnoraApiV2Complex.Error -> JsonLDString(makeClientErrorMessage(ex, appConfig))) ), context = JsonLDObject( Map( @@ -157,14 +157,14 @@ object KnoraExceptionHandler extends LazyLogging { * @param ex the exception to be converted. * @return an [[HttpResponse]] in JSON format. */ - private def exceptionToJsonHttpResponseADM(ex: Throwable, settings: KnoraSettingsImpl): HttpResponse = { + private def exceptionToJsonHttpResponseADM(ex: Throwable, appConfig: AppConfig): HttpResponse = { // Get the HTTP status code that corresponds to the exception. val httpStatus: StatusCode = ApiStatusCodesV2.fromException(ex) // Generate an HTTP response containing the error message ... val responseFields: Map[String, JsValue] = Map( - "error" -> JsString(makeClientErrorMessage(ex, settings)) + "error" -> JsString(makeClientErrorMessage(ex, appConfig)) ) // ... and the HTTP status code. @@ -177,16 +177,16 @@ object KnoraExceptionHandler extends LazyLogging { /** * Given an exception, returns an error message suitable for clients. * - * @param ex the exception. - * @param settings the application settings. + * @param ex the exception. + * @param appConfig the application's configuration. * @return an error message suitable for clients. */ - private def makeClientErrorMessage(ex: Throwable, settings: KnoraSettingsImpl): String = + private def makeClientErrorMessage(ex: Throwable, appConfig: AppConfig): String = ex match { case rre: RequestRejectedException => rre.toString case other => - if (settings.showInternalErrors) { + if (appConfig.showInternalErrors) { other.toString } else { GENERIC_INTERNAL_SERVER_ERROR_MESSAGE 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 dbb061197e..4a9afa57b0 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -43,7 +43,6 @@ 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.settings.KnoraSettingsImpl import org.knora.webapi.util.Base64UrlCheckDigit import org.knora.webapi.util.JavaUtil @@ -225,7 +224,7 @@ object StringFormatter { /** * Initialises the general instance of [[StringFormatter]]. * - * @param settings the application settings. + * @param config the application's configuration. */ def init(config: AppConfig): Unit = this.synchronized { @@ -2594,12 +2593,10 @@ class StringFormatter private ( /** * Constructs a path for accessing a file that has been uploaded to Sipi's temporary storage. * - * @param settings the application settings. * @param filename the filename. * @return a URL for accessing the file. */ - def makeSipiTempFilePath(settings: KnoraSettingsImpl, filename: String): String = - s"/tmp/$filename" + def makeSipiTempFilePath(filename: String): String = s"/tmp/$filename" /** * Checks whether an IRI already exists in the triplestore. 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 2b774df581..527796282f 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 @@ -8,7 +8,6 @@ package org.knora.webapi.messages.util import akka.actor.ActorRef import akka.actor.ActorSystem -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.store.cache.settings.CacheServiceSettings /** @@ -16,12 +15,10 @@ import org.knora.webapi.store.cache.settings.CacheServiceSettings * * @param system the actor system. * @param appActor the main application actor. - * @param knoraSetting the application settings. * @param cacheServiceSettings the cache service part of the settings. */ case class ResponderData( system: ActorSystem, appActor: ActorRef, - knoraSettings: KnoraSettingsImpl, cacheServiceSettings: CacheServiceSettings ) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala index ff812b14dc..9e7035ead4 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala @@ -23,10 +23,10 @@ object RdfFeatureFactory { /** * Initialises the [[RdfFeatureFactory]]. This method must be called once, on application startup. * - * @param settings the application settings. + * @param config the application configuration. */ def init(config: AppConfig): Unit = - // Construct the SHACL validators, which need the application settings. + // Construct the SHACL validators this.synchronized { jenaShaclValidator = Some( new JenaShaclValidator( diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/FullTextMainQueryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/FullTextMainQueryGenerator.scala index 76bef5b3a3..c4a3201429 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/FullTextMainQueryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/FullTextMainQueryGenerator.scala @@ -6,10 +6,10 @@ package org.knora.webapi.messages.util.search import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.settings.KnoraSettingsImpl object FullTextMainQueryGenerator { @@ -64,6 +64,7 @@ object FullTextMainQueryGenerator { * @param valueObjectIris the IRIs of the value objects to be queried. * @param targetSchema the target API schema. * @param schemaOptions the schema options submitted with the request. + * @param appConfig the application's configuration. * @return a [[ConstructQuery]]. */ def createMainQuery( @@ -71,7 +72,7 @@ object FullTextMainQueryGenerator { valueObjectIris: Set[IRI], targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], - settings: KnoraSettingsImpl + appConfig: AppConfig ): ConstructQuery = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -175,7 +176,7 @@ object FullTextMainQueryGenerator { leftArg = standoffStartIndexVar, operator = CompareExpressionOperator.LESS_THAN_OR_EQUAL_TO, rightArg = XsdLiteral( - value = (settings.standoffPerPage - 1).toString, + value = (appConfig.standoffPerPage - 1).toString, datatype = OntologyConstants.Xsd.Integer.toSmartIri ) ) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala index 9489267b25..a4c9574269 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala @@ -7,6 +7,7 @@ package org.knora.webapi.messages.util.search.gravsearch.mainquery import dsp.errors.GravsearchException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter @@ -16,7 +17,6 @@ import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.messages.util.search._ import org.knora.webapi.messages.util.search.gravsearch.prequery.AbstractPrequeryGenerator import org.knora.webapi.messages.util.search.gravsearch.prequery.NonTriplestoreSpecificGravsearchToPrequeryTransformer -import org.knora.webapi.settings.KnoraSettingsImpl object GravsearchMainQueryGenerator { @@ -225,6 +225,7 @@ object GravsearchMainQueryGenerator { * @param valueObjectIris IRIs of value objects to be queried (for both main and dependent resources) * @param targetSchema the target API schema. * @param schemaOptions the schema options submitted with the request. + * @param appConfig the application's configuration. * @return the main [[ConstructQuery]] query to be executed. */ def createMainQuery( @@ -233,7 +234,7 @@ object GravsearchMainQueryGenerator { valueObjectIris: Set[IRI], targetSchema: ApiV2Schema, schemaOptions: Set[SchemaOption], - settings: KnoraSettingsImpl + appConfig: AppConfig ): ConstructQuery = { import GravsearchConstants._ @@ -406,7 +407,7 @@ object GravsearchMainQueryGenerator { leftArg = standoffStartIndexVar, operator = CompareExpressionOperator.LESS_THAN_OR_EQUAL_TO, rightArg = XsdLiteral( - value = (settings.standoffPerPage - 1).toString, + value = (appConfig.standoffPerPage - 1).toString, datatype = OntologyConstants.Xsd.Integer.toSmartIri ) ) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala index 6ec1b456dc..4a2d27af57 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformer.scala @@ -10,13 +10,13 @@ import scala.concurrent.ExecutionContext import dsp.errors.AssertionException import dsp.errors.GravsearchException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.search._ import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInspectionResult import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInspectionUtil import org.knora.webapi.messages.util.search.gravsearch.types.NonPropertyTypeInfo import org.knora.webapi.messages.util.search.gravsearch.types.PropertyTypeInfo -import org.knora.webapi.settings.KnoraSettingsImpl /** * Transforms a preprocessed CONSTRUCT query into a SELECT query that returns only the IRIs and sort order of the main resources that matched @@ -26,13 +26,13 @@ import org.knora.webapi.settings.KnoraSettingsImpl * @param constructClause the CONSTRUCT clause from the input query. * @param typeInspectionResult the result of type inspection of the input query. * @param querySchema the ontology schema used in the input query. - * @param settings application settings. + * @param appConfig application configuration. */ class NonTriplestoreSpecificGravsearchToPrequeryTransformer( constructClause: ConstructClause, typeInspectionResult: GravsearchTypeInspectionResult, querySchema: ApiV2Schema, - settings: KnoraSettingsImpl + appConfig: AppConfig ) extends AbstractPrequeryGenerator( constructClause = constructClause, typeInspectionResult = typeInspectionResult, @@ -355,8 +355,8 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformer( * @return the LIMIT, if any. */ def getLimit: Int = - // get LIMIT from settings - settings.v2ResultsPerPage + // get LIMIT from appConfig + appConfig.v2.resourcesSequence.resultsPerPage /** * Gets the OFFSET to be used in the prequery (needed for paging). diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraRequestV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraRequestV2.scala index 4051445460..277fed70e6 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraRequestV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/KnoraRequestV2.scala @@ -18,7 +18,6 @@ import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.messages.util.rdf.RdfFeatureFactory import org.knora.webapi.messages.util.rdf.RdfModel import org.knora.webapi.messages.util.rdf.Turtle -import org.knora.webapi.settings.KnoraSettingsImpl /** * A trait for request messages that are constructed as an [[RdfModel]]. @@ -57,7 +56,6 @@ trait KnoraJsonLDRequestReaderV2[C] { * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActor a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a case class instance representing the input. */ @@ -66,7 +64,6 @@ trait KnoraJsonLDRequestReaderV2[C] { apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[C] } 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 24f46669c3..189b980774 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 @@ -14,10 +14,10 @@ import org.knora.webapi.InternalSchema import org.knora.webapi.OntologySchema import org.knora.webapi.SchemaOption import org.knora.webapi.SchemaOptions +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.util.rdf._ -import org.knora.webapi.settings.KnoraSettingsImpl /** * A trait for Knora API V2 response messages. @@ -30,14 +30,14 @@ trait KnoraResponseV2 { * @param rdfFormat the RDF format selected for the response. * @param targetSchema the response schema. * @param schemaOptions the schema options. - * @param settings the application settings. + * @param appConfig the application configuration. * @return a formatted string representing this response message. */ def format( rdfFormat: RdfFormat, targetSchema: OntologySchema, schemaOptions: Set[SchemaOption], - settings: KnoraSettingsImpl + appConfig: AppConfig ): String } @@ -50,7 +50,7 @@ trait KnoraJsonLDResponseV2 extends KnoraResponseV2 { rdfFormat: RdfFormat, targetSchema: OntologySchema, schemaOptions: Set[SchemaOption], - settings: KnoraSettingsImpl + appConfig: AppConfig ): String = { val targetApiV2Schema = targetSchema match { case apiV2Schema: ApiV2Schema => apiV2Schema @@ -60,7 +60,7 @@ trait KnoraJsonLDResponseV2 extends KnoraResponseV2 { // Convert this response message to a JsonLDDocument. val jsonLDDocument: JsonLDDocument = toJsonLDDocument( targetSchema = targetApiV2Schema, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) @@ -92,7 +92,7 @@ trait KnoraJsonLDResponseV2 extends KnoraResponseV2 { */ protected def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument } @@ -112,7 +112,7 @@ trait KnoraTurtleResponseV2 extends KnoraResponseV2 { rdfFormat: RdfFormat, targetSchema: OntologySchema, schemaOptions: Set[SchemaOption], - settings: KnoraSettingsImpl + appConfig: AppConfig ): String = { if (targetSchema != InternalSchema) { throw AssertionException(s"Response can be returned only in the internal schema") @@ -147,7 +147,7 @@ trait KnoraTurtleResponseV2 extends KnoraResponseV2 { case class SuccessResponseV2(message: String) extends KnoraJsonLDResponseV2 { def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { val (ontologyPrefixExpansion, resultProp) = targetSchema match { @@ -176,7 +176,7 @@ case class SuccessResponseV2(message: String) extends KnoraJsonLDResponseV2 { final case class CanDoResponseV2(canDo: Boolean) extends KnoraJsonLDResponseV2 { def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { if (targetSchema != ApiV2Complex) { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/listsmessages/ListsMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/listsmessages/ListsMessagesV2.scala index 8d3e1f697f..b24063318a 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/listsmessages/ListsMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/listsmessages/ListsMessagesV2.scala @@ -6,6 +6,7 @@ package org.knora.webapi.messages.v2.responder.listsmessages import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 @@ -17,7 +18,6 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.util.rdf import org.knora.webapi.messages.util.rdf._ import org.knora.webapi.messages.v2.responder.KnoraJsonLDResponseV2 -import org.knora.webapi.settings.KnoraSettingsImpl /** * An abstract trait representing a Knora v2 API request message that can be sent to `ListsResponderV2`. @@ -73,7 +73,7 @@ case class ListGetResponseV2(list: ListADM, userLang: String, fallbackLang: Stri def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { @@ -201,7 +201,7 @@ case class NodeGetResponseV2(node: ListNodeInfoADM, userLang: String, fallbackLa def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala index 3103ba5fc2..49e87a4ffd 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/ontologymessages/OntologyMessagesV2.scala @@ -25,6 +25,7 @@ import dsp.schema.domain.Cardinality._ import dsp.valueobjects.Iri import dsp.valueobjects.Schema import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 @@ -37,7 +38,6 @@ import org.knora.webapi.messages.v2.responder._ import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.KnoraCardinalityInfo import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.OwlCardinalityInfo import org.knora.webapi.messages.v2.responder.standoffmessages.StandoffDataTypeClasses -import org.knora.webapi.settings.KnoraSettingsImpl /** * An abstract trait for messages that can be sent to `ResourcesResponderV2`. @@ -88,7 +88,6 @@ object CreateOntologyRequestV2 extends KnoraJsonLDRequestReaderV2[CreateOntology * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActor a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[CreateOntologyRequestV2]] representing the input. */ @@ -97,7 +96,6 @@ object CreateOntologyRequestV2 extends KnoraJsonLDRequestReaderV2[CreateOntology apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[CreateOntologyRequestV2] = Future { @@ -398,7 +396,6 @@ object CreatePropertyRequestV2 extends KnoraJsonLDRequestReaderV2[CreateProperty * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[CreatePropertyRequestV2]] representing the input. */ @@ -407,7 +404,6 @@ object CreatePropertyRequestV2 extends KnoraJsonLDRequestReaderV2[CreateProperty apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[CreatePropertyRequestV2] = Future { @@ -495,7 +491,6 @@ object CreateClassRequestV2 extends KnoraJsonLDRequestReaderV2[CreateClassReques * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[CreateClassRequestV2]] representing the input. */ @@ -504,7 +499,6 @@ object CreateClassRequestV2 extends KnoraJsonLDRequestReaderV2[CreateClassReques apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[CreateClassRequestV2] = Future { @@ -571,7 +565,6 @@ object AddCardinalitiesToClassRequestV2 extends KnoraJsonLDRequestReaderV2[AddCa * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return an [[AddCardinalitiesToClassRequestV2]] representing the input. */ @@ -580,7 +573,6 @@ object AddCardinalitiesToClassRequestV2 extends KnoraJsonLDRequestReaderV2[AddCa apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[AddCardinalitiesToClassRequestV2] = Future { @@ -656,7 +648,6 @@ object ChangeCardinalitiesRequestV2 extends KnoraJsonLDRequestReaderV2[ChangeCar * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[ChangeCardinalitiesRequestV2]] representing the input. */ @@ -665,7 +656,6 @@ object ChangeCardinalitiesRequestV2 extends KnoraJsonLDRequestReaderV2[ChangeCar apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ChangeCardinalitiesRequestV2] = Future { @@ -724,7 +714,6 @@ object CanDeleteCardinalitiesFromClassRequestV2 * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[DeleteCardinalitiesFromClassRequestV2]] representing the input. */ @@ -733,7 +722,6 @@ object CanDeleteCardinalitiesFromClassRequestV2 apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[CanDeleteCardinalitiesFromClassRequestV2] = Future { @@ -791,7 +779,6 @@ object DeleteCardinalitiesFromClassRequestV2 extends KnoraJsonLDRequestReaderV2[ * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[DeleteCardinalitiesFromClassRequestV2]] representing the input. */ @@ -800,7 +787,6 @@ object DeleteCardinalitiesFromClassRequestV2 extends KnoraJsonLDRequestReaderV2[ apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DeleteCardinalitiesFromClassRequestV2] = Future { @@ -927,7 +913,6 @@ object ChangePropertyGuiElementRequest extends KnoraJsonLDRequestReaderV2[Change * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActor a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[ChangePropertyLabelsOrCommentsRequestV2]] representing the input. */ @@ -937,7 +922,6 @@ object ChangePropertyGuiElementRequest extends KnoraJsonLDRequestReaderV2[Change apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ChangePropertyGuiElementRequest] = Future { @@ -980,7 +964,6 @@ object ChangePropertyLabelsOrCommentsRequestV2 * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[ChangePropertyLabelsOrCommentsRequestV2]] representing the input. */ @@ -989,7 +972,6 @@ object ChangePropertyLabelsOrCommentsRequestV2 apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ChangePropertyLabelsOrCommentsRequestV2] = Future { @@ -1051,7 +1033,6 @@ object DeletePropertyCommentRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteP * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActor a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[DeletePropertyCommentRequestV2]] representing the input. */ @@ -1060,7 +1041,6 @@ object DeletePropertyCommentRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteP apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DeletePropertyCommentRequestV2] = Future { @@ -1122,7 +1102,6 @@ object ChangeClassLabelsOrCommentsRequestV2 extends KnoraJsonLDRequestReaderV2[C * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[ChangeClassLabelsOrCommentsRequestV2]] representing the input. */ @@ -1131,7 +1110,6 @@ object ChangeClassLabelsOrCommentsRequestV2 extends KnoraJsonLDRequestReaderV2[C apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ChangeClassLabelsOrCommentsRequestV2] = Future { @@ -1193,7 +1171,6 @@ object DeleteClassCommentRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteClas * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActor a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[DeleteClassCommentRequestV2]] representing the input. */ @@ -1202,7 +1179,6 @@ object DeleteClassCommentRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteClas apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DeleteClassCommentRequestV2] = Future { @@ -1245,7 +1221,6 @@ object ChangeGuiOrderRequestV2 extends KnoraJsonLDRequestReaderV2[ChangeGuiOrder apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ChangeGuiOrderRequestV2] = Future { @@ -1314,7 +1289,6 @@ object ChangeOntologyMetadataRequestV2 extends KnoraJsonLDRequestReaderV2[Change * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[ChangeClassLabelsOrCommentsRequestV2]] representing the input. */ @@ -1323,7 +1297,6 @@ object ChangeOntologyMetadataRequestV2 extends KnoraJsonLDRequestReaderV2[Change apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ChangeOntologyMetadataRequestV2] = Future { @@ -1668,12 +1641,12 @@ case class ReadOntologyV2( override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = - toOntologySchema(targetSchema).generateJsonLD(targetSchema, settings) + toOntologySchema(targetSchema).generateJsonLD(targetSchema, appConfig) - private def generateJsonLD(targetSchema: ApiV2Schema, settings: KnoraSettingsImpl): JsonLDDocument = { + private def generateJsonLD(targetSchema: ApiV2Schema, appConfig: AppConfig): JsonLDDocument = { // Get the ontologies of all Knora entities mentioned in class definitions. val knoraOntologiesFromClasses: Set[SmartIri] = classes.values.flatMap { classInfo => @@ -1748,7 +1721,7 @@ case class ReadOntologyV2( val jsonClasses: Vector[JsonLDObject] = classes.values.map { readClassInfo => userLang match { case Some(lang) => - readClassInfo.toJsonLDWithSingleLanguage(targetSchema = targetSchema, userLang = lang, settings = settings) + readClassInfo.toJsonLDWithSingleLanguage(targetSchema = targetSchema, userLang = lang, appConfig = appConfig) case None => readClassInfo.toJsonLDWithAllLanguages(targetSchema = targetSchema) } }.toVector @@ -1756,7 +1729,11 @@ case class ReadOntologyV2( val jsonProperties: Vector[JsonLDObject] = properties.values.map { readPropertyInfo => userLang match { case Some(lang) => - readPropertyInfo.toJsonLDWithSingleLanguage(targetSchema = targetSchema, userLang = lang, settings = settings) + readPropertyInfo.toJsonLDWithSingleLanguage( + targetSchema = targetSchema, + userLang = lang, + appConfig = appConfig + ) case None => readPropertyInfo.toJsonLDWithAllLanguages(targetSchema = targetSchema) } }.toVector @@ -1767,7 +1744,7 @@ case class ReadOntologyV2( readIndividualInfo.toJsonLDWithSingleLanguage( targetSchema = targetSchema, userLang = lang, - settings = settings + appConfig = appConfig ) case None => readIndividualInfo.toJsonLDWithAllLanguages(targetSchema = targetSchema) } @@ -2035,7 +2012,7 @@ case class ReadOntologyMetadataV2(ontologies: Set[OntologyMetadataV2]) def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = toOntologySchema(targetSchema).generateJsonLD(targetSchema) @@ -2297,12 +2274,12 @@ sealed trait EntityInfoContentV2 { */ def getPredicateAndStringLiteralObjectWithLang( predicateIri: SmartIri, - settings: KnoraSettingsImpl, + appConfig: AppConfig, userLang: String ): Option[(SmartIri, String)] = getPredicateStringLiteralObject( predicateIri = predicateIri, - preferredLangs = Some(userLang, settings.fallbackLanguage) + preferredLangs = Some(userLang, appConfig.fallbackLanguage) ).map(obj => predicateIri -> obj) /** @@ -2570,22 +2547,22 @@ sealed trait ReadEntityInfoV2 { * * @param targetSchema the API v2 schema in which the response will be returned. * @param userLang the user's preferred language. - * @param settings the application settings. + * @param appConfig the application's configuration. * @return a JSON-LD object representing the entity. */ def toJsonLDWithSingleLanguage( targetSchema: ApiV2Schema, userLang: String, - settings: KnoraSettingsImpl + appConfig: AppConfig ): JsonLDObject = { val label: Option[(IRI, JsonLDString)] = entityInfoContent - .getPredicateAndStringLiteralObjectWithLang(OntologyConstants.Rdfs.Label.toSmartIri, settings, userLang) + .getPredicateAndStringLiteralObjectWithLang(OntologyConstants.Rdfs.Label.toSmartIri, appConfig, userLang) .map { case (k: SmartIri, v: String) => (k.toString, JsonLDString(v)) } val comment: Option[(IRI, JsonLDString)] = entityInfoContent - .getPredicateAndStringLiteralObjectWithLang(OntologyConstants.Rdfs.Comment.toSmartIri, settings, userLang) + .getPredicateAndStringLiteralObjectWithLang(OntologyConstants.Rdfs.Comment.toSmartIri, appConfig, userLang) .map { case (k: SmartIri, v: String) => (k.toString, JsonLDString(v)) } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index db894412e4..c87e9dbbc2 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -17,6 +17,7 @@ import scala.concurrent.Future import dsp.errors._ import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 @@ -35,7 +36,6 @@ import org.knora.webapi.messages.util.standoff.XMLUtil import org.knora.webapi.messages.v2.responder._ import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.valuemessages._ -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.util._ /** @@ -108,7 +108,7 @@ case class ResourceIIIFManifestGetRequestV2( case class ResourceIIIFManifestGetResponseV2(manifest: JsonLDDocument) extends KnoraJsonLDResponseV2 { override protected def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = manifest } @@ -185,7 +185,7 @@ case class ResourceVersionHistoryResponseV2(history: Seq[ResourceHistoryEntry]) */ override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -271,7 +271,7 @@ case class ResourceTEIGetResponseV2(header: TEIHeader, body: TEIBody) { case class TEIHeader( headerInfo: ReadResourceV2, headerXSLT: Option[String], - settings: KnoraSettingsImpl + appConfig: AppConfig ) { def toXML: String = @@ -280,7 +280,7 @@ case class TEIHeader( // Convert the resource to a JsonLDDocument. val headerJsonLD: JsonLDDocument = - ReadResourcesSequenceV2(Seq(headerInfo)).toJsonLDDocument(ApiV2Complex, settings) + ReadResourcesSequenceV2(Seq(headerInfo)).toJsonLDDocument(ApiV2Complex, appConfig) // Convert the JsonLDDocument to an RdfModel. val rdfModel: RdfModel = headerJsonLD.toRdfModel(rdfFormatUtil.getRdfModelFactory) @@ -422,7 +422,7 @@ case class ReadResourceV2( def toJsonLD( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDObject = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -437,7 +437,7 @@ case class ReadResourceV2( readValue.toJsonLD( targetSchema = targetSchema, projectADM = projectADM, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) } @@ -666,7 +666,6 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a case class instance representing the input. */ @@ -675,7 +674,6 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[CreateResourceRequestV2] = { @@ -784,7 +782,6 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource jsonLDObject = valueJsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -880,7 +877,6 @@ object UpdateResourceMetadataRequestV2 extends KnoraJsonLDRequestReaderV2[Update * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a case class instance representing the input. */ @@ -889,7 +885,6 @@ object UpdateResourceMetadataRequestV2 extends KnoraJsonLDRequestReaderV2[Update apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[UpdateResourceMetadataRequestV2] = Future { @@ -978,7 +973,7 @@ case class UpdateResourceMetadataResponseV2( */ override protected def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { @@ -1067,7 +1062,6 @@ object DeleteOrEraseResourceRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteO * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a case class instance representing the input. */ @@ -1076,7 +1070,6 @@ object DeleteOrEraseResourceRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteO apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DeleteOrEraseResourceRequestV2] = Future { @@ -1164,7 +1157,7 @@ case class ReadResourcesSequenceV2( private def generateJsonLD( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -1174,7 +1167,7 @@ case class ReadResourcesSequenceV2( val resourcesJsonObjects: Seq[JsonLDObject] = resources.map { resource: ReadResourceV2 => resource.toJsonLD( targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) } @@ -1228,12 +1221,12 @@ case class ReadResourcesSequenceV2( override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] = Set.empty ): JsonLDDocument = toOntologySchema(targetSchema).generateJsonLD( targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) @@ -1373,7 +1366,7 @@ case class GraphEdgeV2(source: IRI, propertyIri: SmartIri, target: IRI) extends case class GraphDataGetResponseV2(nodes: Seq[GraphNodeV2], edges: Seq[GraphEdgeV2], ontologySchema: OntologySchema) extends KnoraJsonLDResponseV2 with KnoraReadV2[GraphDataGetResponseV2] { - private def generateJsonLD(targetSchema: ApiV2Schema, settings: KnoraSettingsImpl): JsonLDDocument = { + private def generateJsonLD(targetSchema: ApiV2Schema): JsonLDDocument = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance val sortedNodesInTargetSchema: Seq[GraphNodeV2] = nodes.map(_.toOntologySchema(targetSchema)).sortBy(_.resourceIri) @@ -1453,10 +1446,10 @@ case class GraphDataGetResponseV2(nodes: Seq[GraphNodeV2], edges: Seq[GraphEdgeV override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = - toOntologySchema(targetSchema).generateJsonLD(targetSchema, settings) + toOntologySchema(targetSchema).generateJsonLD(targetSchema) override def toOntologySchema(targetSchema: ApiV2Schema): GraphDataGetResponseV2 = GraphDataGetResponseV2( @@ -1510,7 +1503,7 @@ case class ResourceEventBody( def toJsonLD( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDObject = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -1523,7 +1516,7 @@ case class ResourceEventBody( .toJsonLDValue( targetSchema = targetSchema, projectADM = projectADM, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) } @@ -1638,7 +1631,7 @@ case class ValueEventBody( def toJsonLD( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDObject = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -1649,7 +1642,7 @@ case class ValueEventBody( .toJsonLDValue( targetSchema = targetSchema, projectADM = projectADM, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) propertyIri.toString -> contentJsonLD @@ -1707,7 +1700,7 @@ case class ResourceAndValueVersionHistoryResponseV2(historyEvents: Seq[ResourceA */ override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -1721,9 +1714,9 @@ case class ResourceAndValueVersionHistoryResponseV2(historyEvents: Seq[ResourceA val historyEventsAsJsonLD: Seq[JsonLDObject] = historyEvents.map { historyEntry: ResourceAndValueHistoryEvent => // convert event body to JsonLD object val eventBodyAsJsonLD: JsonLDObject = historyEntry.eventBody match { - case valueEventBody: ValueEventBody => valueEventBody.toJsonLD(targetSchema, settings, schemaOptions) + case valueEventBody: ValueEventBody => valueEventBody.toJsonLD(targetSchema, appConfig, schemaOptions) case resourceEventBody: ResourceEventBody => - resourceEventBody.toJsonLD(targetSchema, settings, schemaOptions) + resourceEventBody.toJsonLD(targetSchema, appConfig, schemaOptions) case resourceMetadataEventBody: ResourceMetadataEventBody => resourceMetadataEventBody.toJsonLD case _ => throw NotFoundException(s"Event body is missing or has wrong type.") diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/searchmessages/SearchMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/searchmessages/SearchMessagesV2.scala index e718d04dcf..e028921cd5 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/searchmessages/SearchMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/searchmessages/SearchMessagesV2.scala @@ -8,6 +8,7 @@ package org.knora.webapi.messages.v2.responder.searchmessages import org.knora.webapi.ApiV2Schema import org.knora.webapi.IRI import org.knora.webapi.SchemaOption +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 import org.knora.webapi.messages.SmartIri @@ -18,7 +19,6 @@ import org.knora.webapi.messages.util.rdf.JsonLDObject import org.knora.webapi.messages.util.rdf.JsonLDString import org.knora.webapi.messages.util.search.ConstructQuery import org.knora.webapi.messages.v2.responder._ -import org.knora.webapi.settings.KnoraSettingsImpl /** * An abstract trait for messages that can be sent to `SearchResponderV2`. @@ -134,7 +134,7 @@ case class SearchResourceByLabelRequestV2( case class ResourceCountV2(numberOfResources: Int) extends KnoraJsonLDResponseV2 { override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = JsonLDDocument( diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala index 7b504b7326..33e7401bc8 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/standoffmessages/StandoffMessagesV2.scala @@ -17,6 +17,7 @@ import scala.concurrent.Future import dsp.errors.AssertionException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 @@ -28,7 +29,6 @@ import org.knora.webapi.messages.v2.responder.KnoraContentV2 import org.knora.webapi.messages.v2.responder.KnoraJsonLDRequestReaderV2 import org.knora.webapi.messages.v2.responder.KnoraJsonLDResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages.StandoffEntityInfoGetResponseV2 -import org.knora.webapi.settings.KnoraSettingsImpl /** * An abstract trait representing a Knora v2 API request message that can be sent to `StandoffResponderV2`. @@ -84,7 +84,7 @@ case class GetStandoffResponseV2(valueIri: IRI, standoff: Seq[StandoffTagV2], ne */ override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { if (targetSchema != ApiV2Complex) { @@ -153,7 +153,6 @@ object CreateMappingRequestMetadataV2 extends KnoraJsonLDRequestReaderV2[CreateM apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[CreateMappingRequestMetadataV2] = Future { @@ -211,7 +210,7 @@ case class CreateMappingResponseV2(mappingIri: IRI, label: String, projectIri: S def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index 43760196d5..daa5e3e256 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -21,6 +21,7 @@ import dsp.errors.BadRequestException import dsp.errors.NotImplementedException import dsp.valueobjects.IriErrorMessages import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 @@ -39,8 +40,6 @@ import org.knora.webapi.messages.util.standoff.XMLUtil import org.knora.webapi.messages.v2.responder._ import org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourceV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ -import org.knora.webapi.settings.KnoraSettingsImpl -import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util._ /** @@ -74,7 +73,6 @@ object CreateValueRequestV2 extends KnoraJsonLDRequestReaderV2[CreateValueReques * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a case class instance representing the input. */ @@ -83,7 +81,6 @@ object CreateValueRequestV2 extends KnoraJsonLDRequestReaderV2[CreateValueReques apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[CreateValueRequestV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -108,7 +105,6 @@ object CreateValueRequestV2 extends KnoraJsonLDRequestReaderV2[CreateValueReques jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -188,7 +184,7 @@ case class CreateValueResponseV2( with UpdateResultInProject { override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -244,7 +240,6 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a case class instance representing the input. */ @@ -253,7 +248,6 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[UpdateValueRequestV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -352,7 +346,6 @@ object UpdateValueRequestV2 extends KnoraJsonLDRequestReaderV2[UpdateValueReques jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -394,7 +387,7 @@ case class UpdateValueResponseV2(valueIri: IRI, valueType: SmartIri, valueUUID: with UpdateResultInProject { override def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -455,7 +448,6 @@ object DeleteValueRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteValueReques * @param apiRequestID the UUID of the API request. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a case class instance representing the input. */ @@ -464,7 +456,6 @@ object DeleteValueRequestV2 extends KnoraJsonLDRequestReaderV2[DeleteValueReques apiRequestID: UUID, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DeleteValueRequestV2] = Future { @@ -690,13 +681,12 @@ sealed trait ReadValueV2 extends IOValueV2 { * Converts this value to JSON-LD. * * @param targetSchema the target schema. - * @param settings the application settings. * @return a JSON-LD representation of this value. */ def toJsonLD( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -704,7 +694,7 @@ sealed trait ReadValueV2 extends IOValueV2 { val valueContentAsJsonLD = valueContent.toJsonLDValue( targetSchema = targetSchema, projectADM = projectADM, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) @@ -826,7 +816,7 @@ case class ReadTextValueV2( override def toJsonLD( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -834,7 +824,7 @@ case class ReadTextValueV2( val valueAsJsonLDValue: JsonLDValue = super.toJsonLD( targetSchema = targetSchema, projectADM = projectADM, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) @@ -1103,13 +1093,12 @@ sealed trait ValueContentV2 extends KnoraContentV2[ValueContentV2] { * A representation of the `ValueContentV2` as a [[JsonLDValue]]. * * @param targetSchema the API schema to be used. - * @param settings the configuration options. * @return a [[JsonLDValue]] that can be used to generate JSON-LD representing this value. */ def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue @@ -1154,7 +1143,6 @@ trait ValueContentReaderV2[C <: ValueContentV2] { * @param jsonLDObject the JSON-LD object. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a subclass of [[ValueContentV2]]. */ @@ -1162,7 +1150,6 @@ trait ValueContentReaderV2[C <: ValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[C] @@ -1184,7 +1171,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { * @param jsonLDObject the JSON-LD object. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[ValueContentV2]]. */ @@ -1192,7 +1178,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -1210,7 +1195,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1219,7 +1203,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1228,7 +1211,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1237,7 +1219,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1246,7 +1227,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1255,7 +1235,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1264,7 +1243,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1273,7 +1251,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1282,7 +1259,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1291,7 +1267,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1300,7 +1275,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1309,7 +1283,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1318,7 +1291,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1327,7 +1299,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1336,7 +1307,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1345,7 +1315,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1354,7 +1323,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1363,7 +1331,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1372,7 +1339,6 @@ object ValueContentV2 extends ValueContentReaderV2[ValueContentV2] { jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -1438,7 +1404,7 @@ case class DateValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -1543,8 +1509,7 @@ object DateValueContentV2 extends ValueContentReaderV2[DateValueContentV2] { * * @param jsonLDObject the JSON-LD object. * @param requestingUser the user making the request. - * @param appActror a reference to the application actor. - * @param settings the application settings. + * @param appActor a reference to the application actor. * @param log a logging adapter. * @return a [[DateValueContentV2]]. */ @@ -1552,7 +1517,6 @@ object DateValueContentV2 extends ValueContentReaderV2[DateValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DateValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -1729,7 +1693,7 @@ case class TextValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -1940,7 +1904,6 @@ object TextValueContentV2 extends ValueContentReaderV2[TextValueContentV2] { * @param jsonLDObject the JSON-LD object. * @param requestingUser the user making the request. * @param appActror a reference to the application actor. - * @param settings the application settings. * @param log a logging adapter. * @return a [[TextValueContentV2]]. */ @@ -1948,7 +1911,6 @@ object TextValueContentV2 extends ValueContentReaderV2[TextValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[TextValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -2056,7 +2018,7 @@ case class IntegerValueContentV2(ontologySchema: OntologySchema, valueHasInteger override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -2107,7 +2069,6 @@ object IntegerValueContentV2 extends ValueContentReaderV2[IntegerValueContentV2] jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[IntegerValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2149,7 +2110,7 @@ case class DecimalValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { val decimalValueAsJsonLDObject = JsonLDUtil.datatypeValueToJsonLDObject( @@ -2205,7 +2166,6 @@ object DecimalValueContentV2 extends ValueContentReaderV2[DecimalValueContentV2] jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DecimalValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2251,7 +2211,7 @@ case class BooleanValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -2299,7 +2259,6 @@ object BooleanValueContentV2 extends ValueContentReaderV2[BooleanValueContentV2] jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[BooleanValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2338,7 +2297,7 @@ case class GeomValueContentV2(ontologySchema: OntologySchema, valueHasGeometry: override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -2395,7 +2354,6 @@ object GeomValueContentV2 extends ValueContentReaderV2[GeomValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[GeomValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2442,7 +2400,7 @@ case class IntervalValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -2513,7 +2471,6 @@ object IntervalValueContentV2 extends ValueContentReaderV2[IntervalValueContentV jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[IntervalValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2565,7 +2522,7 @@ case class TimeValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -2629,7 +2586,6 @@ object TimeValueContentV2 extends ValueContentReaderV2[TimeValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[TimeValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2677,7 +2633,7 @@ case class HierarchicalListValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -2744,7 +2700,6 @@ object HierarchicalListValueContentV2 extends ValueContentReaderV2[HierarchicalL jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[HierarchicalListValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2789,7 +2744,7 @@ case class ColorValueContentV2(ontologySchema: OntologySchema, valueHasColor: St override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -2846,7 +2801,6 @@ object ColorValueContentV2 extends ValueContentReaderV2[ColorValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ColorValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2887,7 +2841,7 @@ case class UriValueContentV2(ontologySchema: OntologySchema, valueHasUri: String override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { val uriAsJsonLDObject = JsonLDUtil.datatypeValueToJsonLDObject( @@ -2946,7 +2900,6 @@ object UriValueContentV2 extends ValueContentReaderV2[UriValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[UriValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -2992,7 +2945,7 @@ case class GeonameValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -3051,7 +3004,6 @@ object GeonameValueContentV2 extends ValueContentReaderV2[GeonameValueContentV2] jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[GeonameValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -3098,7 +3050,6 @@ object FileValueWithSipiMetadata { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[FileValueWithSipiMetadata] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3113,7 +3064,7 @@ object FileValueWithSipiMetadata { ) // Ask Sipi about the rest of the file's metadata. - tempFilePath = stringFormatter.makeSipiTempFilePath(settings, internalFilename) + tempFilePath = stringFormatter.makeSipiTempFilePath(internalFilename) fileMetadataResponse: GetFileMetadataResponse <- appActor .ask( GetFileMetadataRequest( @@ -3186,21 +3137,16 @@ case class StillImageFileValueContentV2( override def toOntologySchema(targetSchema: OntologySchema): StillImageFileValueContentV2 = copy(ontologySchema = targetSchema) - def makeFileUrl(projectADM: ProjectADM, settings: KnoraSettingsImpl): String = - s"${settings.externalSipiIIIFGetUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/full/$dimX,$dimY/0/default.jpg" - - // def makeFileUrl(projectADM: ProjectADM, appConfig: AppConfig): String = - // s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/full/$dimX,$dimY/0/default.jpg" + def makeFileUrl(projectADM: ProjectADM, url: String): String = + s"${url}/${projectADM.shortcode}/${fileValue.internalFilename}/full/$dimX,$dimY/0/default.jpg" override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, - // appConfig: AppConfig, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { - val fileUrl: String = makeFileUrl(projectADM, settings) - // val fileUrl: String = makeFileUrl(projectADM, appConfig) + val fileUrl: String = makeFileUrl(projectADM, appConfig.sipi.externalBaseUrl) targetSchema match { case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) @@ -3212,8 +3158,7 @@ case class StillImageFileValueContentV2( OntologyConstants.KnoraApiV2Complex.StillImageFileValueHasDimY -> JsonLDInt(dimY), OntologyConstants.KnoraApiV2Complex.StillImageFileValueHasIIIFBaseUrl -> JsonLDUtil .datatypeValueToJsonLDObject( - value = s"${settings.externalSipiIIIFGetUrl}/${projectADM.shortcode}", - // value = s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}", + value = s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}", datatype = OntologyConstants.Xsd.Uri.toSmartIri ) ) @@ -3241,6 +3186,7 @@ case class StillImageFileValueContentV2( case _ => throw AssertionException(s"Can't compare a <$valueType> to a <${currentVersion.valueType}>") } + } /** @@ -3251,7 +3197,6 @@ object StillImageFileValueContentV2 extends ValueContentReaderV2[StillImageFileV jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[StillImageFileValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3262,22 +3207,13 @@ object StillImageFileValueContentV2 extends ValueContentReaderV2[StillImageFileV jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) - - _ = if (!settings.imageMimeTypes.contains(fileValueWithSipiMetadata.fileValue.internalMimeType)) { - throw BadRequestException( - s"File ${fileValueWithSipiMetadata.fileValue.internalFilename} has MIME type ${fileValueWithSipiMetadata.fileValue.internalMimeType}, which is not supported for still image files" - ) - } } yield StillImageFileValueContentV2( ontologySchema = ApiV2Complex, fileValue = fileValueWithSipiMetadata.fileValue, - dimX = fileValueWithSipiMetadata.sipiFileMetadata.width - .getOrElse(throw SipiException(s"Sipi did not return the image width")), - dimY = fileValueWithSipiMetadata.sipiFileMetadata.height - .getOrElse(throw SipiException(s"Sipi did not return the image height")), + dimX = fileValueWithSipiMetadata.sipiFileMetadata.width.getOrElse(0), + dimY = fileValueWithSipiMetadata.sipiFileMetadata.height.getOrElse(0), comment = getComment(jsonLDObject) ) } @@ -3313,10 +3249,11 @@ case class DocumentFileValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { - val fileUrl: String = s"${settings.externalSipiBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" + val fileUrl: String = + s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" targetSchema match { case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) @@ -3386,10 +3323,11 @@ case class ArchiveFileValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { - val fileUrl: String = s"${settings.externalSipiBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" + val fileUrl: String = + s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" targetSchema match { case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) @@ -3430,7 +3368,6 @@ object DocumentFileValueContentV2 extends ValueContentReaderV2[DocumentFileValue jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[DocumentFileValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3440,15 +3377,9 @@ object DocumentFileValueContentV2 extends ValueContentReaderV2[DocumentFileValue jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) - _ = if (!settings.documentMimeTypes.contains(fileValueWithSipiMetadata.fileValue.internalMimeType)) { - throw BadRequestException( - s"File ${fileValueWithSipiMetadata.fileValue.internalFilename} has MIME type ${fileValueWithSipiMetadata.fileValue.internalMimeType}, which is not supported for document files" - ) - } } yield DocumentFileValueContentV2( ontologySchema = ApiV2Complex, fileValue = fileValueWithSipiMetadata.fileValue, @@ -3468,7 +3399,6 @@ object ArchiveFileValueContentV2 extends ValueContentReaderV2[ArchiveFileValueCo jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[ArchiveFileValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3478,15 +3408,9 @@ object ArchiveFileValueContentV2 extends ValueContentReaderV2[ArchiveFileValueCo jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) - _ = if (!settings.archiveMimeTypes.contains(fileValueWithSipiMetadata.fileValue.internalMimeType)) { - throw BadRequestException( - s"File ${fileValueWithSipiMetadata.fileValue.internalFilename} has MIME type ${fileValueWithSipiMetadata.fileValue.internalMimeType}, which is not supported for archive files" - ) - } } yield ArchiveFileValueContentV2( ontologySchema = ApiV2Complex, fileValue = fileValueWithSipiMetadata.fileValue, @@ -3519,10 +3443,11 @@ case class TextFileValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { - val fileUrl: String = s"${settings.externalSipiBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" + val fileUrl: String = + s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" targetSchema match { case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) @@ -3561,7 +3486,6 @@ object TextFileValueContentV2 extends ValueContentReaderV2[TextFileValueContentV jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[TextFileValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3571,15 +3495,9 @@ object TextFileValueContentV2 extends ValueContentReaderV2[TextFileValueContentV jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) - _ = if (!settings.textMimeTypes.contains(fileValueWithSipiMetadata.fileValue.internalMimeType)) { - throw BadRequestException( - s"File ${fileValueWithSipiMetadata.fileValue.internalFilename} has MIME type ${fileValueWithSipiMetadata.fileValue.internalMimeType}, which is not supported for text files" - ) - } } yield TextFileValueContentV2( ontologySchema = ApiV2Complex, fileValue = fileValueWithSipiMetadata.fileValue, @@ -3612,10 +3530,11 @@ case class AudioFileValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { - val fileUrl: String = s"${settings.externalSipiBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" + val fileUrl: String = + s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" targetSchema match { case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) @@ -3654,7 +3573,6 @@ object AudioFileValueContentV2 extends ValueContentReaderV2[AudioFileValueConten jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[AudioFileValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3664,15 +3582,9 @@ object AudioFileValueContentV2 extends ValueContentReaderV2[AudioFileValueConten jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) - _ = if (!settings.audioMimeTypes.contains(fileValueWithSipiMetadata.fileValue.internalMimeType)) { - throw BadRequestException( - s"File ${fileValueWithSipiMetadata.fileValue.internalFilename} has MIME type ${fileValueWithSipiMetadata.fileValue.internalMimeType}, which is not supported for audio files" - ) - } } yield AudioFileValueContentV2( ontologySchema = ApiV2Complex, fileValue = fileValueWithSipiMetadata.fileValue, @@ -3705,10 +3617,11 @@ case class MovingImageFileValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = { - val fileUrl: String = s"${settings.externalSipiBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" + val fileUrl: String = + s"${appConfig.sipi.externalBaseUrl}/${projectADM.shortcode}/${fileValue.internalFilename}/file" targetSchema match { case ApiV2Simple => toJsonLDValueInSimpleSchema(fileUrl) @@ -3749,7 +3662,6 @@ object MovingImageFileValueContentV2 extends ValueContentReaderV2[MovingImageFil jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[MovingImageFileValueContentV2] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -3759,15 +3671,9 @@ object MovingImageFileValueContentV2 extends ValueContentReaderV2[MovingImageFil jsonLDObject = jsonLDObject, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) - _ = if (!settings.videoMimeTypes.contains(fileValueWithSipiMetadata.fileValue.internalMimeType)) { - throw BadRequestException( - s"File ${fileValueWithSipiMetadata.fileValue.internalFilename} has MIME type ${fileValueWithSipiMetadata.fileValue.internalMimeType}, which is not supported for video files" - ) - } } yield MovingImageFileValueContentV2( ontologySchema = ApiV2Complex, fileValue = fileValueWithSipiMetadata.fileValue, @@ -3821,7 +3727,7 @@ case class LinkValueContentV2( override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = targetSchema match { @@ -3834,7 +3740,7 @@ case class LinkValueContentV2( // include the nested resource in the response val referredResourceAsJsonLDValue: JsonLDObject = targetResource.toJsonLD( targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) @@ -3895,7 +3801,6 @@ object LinkValueContentV2 extends ValueContentReaderV2[LinkValueContentV2] { jsonLDObject: JsonLDObject, requestingUser: UserADM, appActor: ActorRef, - settings: KnoraSettingsImpl, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[LinkValueContentV2] = Future(fromJsonLDObjectSync(jsonLDObject)) @@ -3939,7 +3844,7 @@ case class DeletedValueContentV2(ontologySchema: OntologySchema, comment: Option override def toJsonLDValue( targetSchema: ApiV2Schema, projectADM: ProjectADM, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDValue = JsonLDObject(Map(OntologyConstants.KnoraBase.DeletedValue -> JsonLDString("DeletedValue"))) 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 e283de37c7..3dd82c9178 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -20,7 +20,6 @@ import scala.concurrent.Future import dsp.errors._ import org.knora.webapi.config.AppConfig import org.knora.webapi.settings.KnoraDispatchers -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.store.cache.settings.CacheServiceSettings import messages.store.triplestoremessages.SparqlSelectRequest @@ -64,11 +63,6 @@ abstract class Responder(responderData: ResponderData, appConfig: AppConfig) ext protected implicit val executionContext: ExecutionContext = system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) - /** - * The application settings. - */ - protected val settings: KnoraSettingsImpl = responderData.knoraSettings - /** * The Cache Service settings. */ 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 ba0dd27180..58fc8161e7 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 @@ -183,7 +183,6 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) ) // Check link targets and list nodes that should exist. - _ <- checkStandoffLinkTargets( values = internalCreateResource.flatValues, requestingUser = createResourceRequestV2.requestingUser @@ -200,7 +199,6 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) // Get the definitions of the resource class and its properties, as well as of the classes of all // resources that are link targets. - resourceClassEntityInfoResponse: EntityInfoGetResponseV2 <- appActor .ask( @@ -2080,7 +2078,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) header = TEIHeader( headerInfo = headerResource, headerXSLT = headerXSLT, - settings = settings + appConfig = appConfig ), body = TEIBody( bodyInfo = bodyTextValue, @@ -2569,7 +2567,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) } val fileUrl: String = - imageValueContent.makeFileUrl(projectADM = representation.projectADM, settings = settings) + imageValueContent.makeFileUrl(projectADM = representation.projectADM, appConfig.sipi.externalBaseUrl) JsonLDObject( Map( 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 91a7282634..7c035e5996 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 @@ -298,7 +298,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) valueObjectIris = allValueObjectIris, targetSchema = targetSchema, schemaOptions = schemaOptions, - settings = settings + appConfig = appConfig ) val queryPatternTransformerConstruct: ConstructToConstructTransformer = @@ -509,7 +509,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) constructClause = inputQuery.constructClause, typeInspectionResult = typeInspectionResult, querySchema = inputQuery.querySchema.getOrElse(throw AssertionException(s"WhereClause has no querySchema")), - settings = settings + appConfig = appConfig ) // TODO: if the ORDER BY criterion is a property whose occurrence is not 1, then the logic does not work correctly @@ -639,7 +639,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) valueObjectIris = allValueObjectIris, targetSchema = targetSchema, schemaOptions = schemaOptions, - settings = settings + appConfig = appConfig ) val queryPatternTransformerConstruct: ConstructToConstructTransformer = @@ -834,8 +834,8 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) resourceClassIri = internalClassIri, maybeOrderByProperty = maybeInternalOrderByPropertyIri, maybeOrderByValuePredicate = maybeOrderByValuePredicate, - limit = settings.v2ResultsPerPage, - offset = resourcesInProjectGetRequestV2.page * settings.v2ResultsPerPage + limit = appConfig.v2.resourcesSequence.resultsPerPage, + offset = resourcesInProjectGetRequestV2.page * appConfig.v2.resourcesSequence.resultsPerPage ) .toString @@ -1012,8 +1012,8 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) searchTerm = searchPhrase, limitToProject = limitToProject, limitToResourceClass = limitToResourceClass.map(_.toString), - limit = settings.v2ResultsPerPage, - offset = offset * settings.v2ResultsPerPage, + limit = appConfig.v2.resourcesSequence.resultsPerPage, + offset = offset * appConfig.v2.resourcesSequence.resultsPerPage, countQuery = false ) .toString() 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 1e0fed172e..51d06789c0 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -11,6 +11,7 @@ import ch.megard.akka.http.cors.scaladsl.CorsDirectives import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings import zio._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.core import org.knora.webapi.core.ActorSystem import org.knora.webapi.core.AppRouter @@ -20,7 +21,6 @@ import org.knora.webapi.routing.AroundDirectives import org.knora.webapi.routing.HealthRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RejectingRoute -import org.knora.webapi.routing.SwaggerApiDocsRoute import org.knora.webapi.routing.VersionRoute import org.knora.webapi.routing.admin.FilesRouteADM import org.knora.webapi.routing.admin.GroupsRouteADM @@ -57,19 +57,21 @@ object ApiRoutes { /** * All routes composed together. */ - val layer: ZLayer[ActorSystem & AppRouter & core.State, Nothing, ApiRoutes] = + val layer: ZLayer[ActorSystem & AppRouter & core.State & AppConfig, Nothing, ApiRoutes] = ZLayer { for { - sys <- ZIO.service[ActorSystem] - router <- ZIO.service[AppRouter] + sys <- ZIO.service[ActorSystem] + router <- ZIO.service[AppRouter] + appConfig <- ZIO.service[AppConfig] routeData <- ZIO.succeed( KnoraRouteData( system = sys.system, - appActor = router.ref + appActor = router.ref, + appConfig = appConfig ) ) runtime <- ZIO.runtime[core.State] - } yield ApiRoutesImpl(routeData, runtime) + } yield ApiRoutesImpl(routeData, runtime, appConfig) } } @@ -80,45 +82,44 @@ object ApiRoutes { * ALL requests go through each of the routes in ORDER. * The FIRST matching route is used for handling a request. */ -private final case class ApiRoutesImpl(routeData: KnoraRouteData, runtime: Runtime[core.State]) +private final case class ApiRoutesImpl(routeData: KnoraRouteData, runtime: Runtime[core.State], appConfig: AppConfig) extends ApiRoutes with AroundDirectives { val routes = logDuration { ServerVersion.addServerHeader { - DSPApiDirectives.handleErrors(routeData.system) { + DSPApiDirectives.handleErrors(routeData.system, appConfig) { CorsDirectives.cors(CorsSettings(routeData.system)) { - DSPApiDirectives.handleErrors(routeData.system) { - new HealthRoute(routeData, runtime).makeRoute ~ + DSPApiDirectives.handleErrors(routeData.system, appConfig) { + new HealthRoute(routeData, runtime, appConfig).makeRoute ~ new VersionRoute().makeRoute ~ - new RejectingRoute(routeData.system, runtime).makeRoute ~ - new ResourcesRouteV1(routeData).makeRoute ~ - new ValuesRouteV1(routeData).makeRoute ~ - new StandoffRouteV1(routeData).makeRoute ~ - new ListsRouteV1(routeData).makeRoute ~ - new ResourceTypesRouteV1(routeData).makeRoute ~ - new SearchRouteV1(routeData).makeRoute ~ - new AuthenticationRouteV1(routeData).makeRoute ~ - new AssetsRouteV1(routeData).makeRoute ~ - new CkanRouteV1(routeData).makeRoute ~ - new UsersRouteV1(routeData).makeRoute ~ - new ProjectsRouteV1(routeData).makeRoute ~ - new OntologiesRouteV2(routeData).makeRoute ~ - new SearchRouteV2(routeData).makeRoute ~ - new ResourcesRouteV2(routeData).makeRoute ~ - new ValuesRouteV2(routeData).makeRoute ~ - new StandoffRouteV2(routeData).makeRoute ~ - new ListsRouteV2(routeData).makeRoute ~ - new AuthenticationRouteV2(routeData).makeRoute ~ - new GroupsRouteADM(routeData).makeRoute ~ - new ListsRouteADM(routeData).makeRoute ~ - new PermissionsRouteADM(routeData).makeRoute ~ - new ProjectsRouteADM(routeData).makeRoute ~ - new StoreRouteADM(routeData).makeRoute ~ - new UsersRouteADM(routeData).makeRoute ~ - new FilesRouteADM(routeData).makeRoute ~ - new SwaggerApiDocsRoute(routeData).makeRoute + new RejectingRoute(routeData.system, runtime, appConfig).makeRoute ~ + new ResourcesRouteV1(routeData, appConfig).makeRoute ~ + new ValuesRouteV1(routeData, appConfig).makeRoute ~ + new StandoffRouteV1(routeData, appConfig).makeRoute ~ + new ListsRouteV1(routeData, appConfig).makeRoute ~ + new ResourceTypesRouteV1(routeData, appConfig).makeRoute ~ + new SearchRouteV1(routeData, appConfig).makeRoute ~ + new AuthenticationRouteV1(routeData, appConfig).makeRoute ~ + new AssetsRouteV1(routeData, appConfig).makeRoute ~ + new CkanRouteV1(routeData, appConfig).makeRoute ~ + new UsersRouteV1(routeData, appConfig).makeRoute ~ + new ProjectsRouteV1(routeData, appConfig).makeRoute ~ + new OntologiesRouteV2(routeData, appConfig).makeRoute ~ + new SearchRouteV2(routeData, appConfig).makeRoute ~ + new ResourcesRouteV2(routeData, appConfig).makeRoute ~ + new ValuesRouteV2(routeData, appConfig).makeRoute ~ + new StandoffRouteV2(routeData, appConfig).makeRoute ~ + new ListsRouteV2(routeData, appConfig).makeRoute ~ + new AuthenticationRouteV2(routeData, appConfig).makeRoute ~ + new GroupsRouteADM(routeData, appConfig).makeRoute ~ + new ListsRouteADM(routeData, appConfig).makeRoute ~ + new PermissionsRouteADM(routeData, appConfig).makeRoute ~ + new ProjectsRouteADM(routeData, appConfig).makeRoute ~ + new StoreRouteADM(routeData, appConfig).makeRoute ~ + new UsersRouteADM(routeData, appConfig).makeRoute ~ + new FilesRouteADM(routeData, appConfig).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 3aa6bb9aa8..1dc46c4435 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala @@ -38,6 +38,7 @@ import dsp.errors.AuthenticationException import dsp.errors.BadCredentialsException import dsp.errors.BadRequestException import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.instrumentation.InstrumentationSupport import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages._ @@ -47,8 +48,6 @@ import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredenti import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraPasswordCredentialsV2 import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraSessionCredentialsV2 import org.knora.webapi.messages.v2.routing.authenticationmessages._ -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.util.cache.CacheUtil /** @@ -76,34 +75,34 @@ trait Authenticator extends InstrumentationSupport { * @return a [[HttpResponse]] containing either a failure message or a message with a cookie header containing * the generated session id. */ - def doLoginV1(requestContext: RequestContext)(implicit + def doLoginV1(requestContext: RequestContext, appConfig: AppConfig)(implicit system: ActorSystem, appActor: ActorRef, executionContext: ExecutionContext ): Future[HttpResponse] = { - val settings = KnoraSettings(system) - val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, settings) + val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, appConfig) for { userADM <- getUserADMThroughCredentialsV2( - credentials = credentials + credentials = credentials, + appConfig ) // will return or throw userProfile = userADM.asUserProfileV1 - cookieDomain = Some(settings.cookieDomain) + cookieDomain = Some(appConfig.cookieDomain) sessionToken = JWTHelper.createToken( userProfile.userData.user_id.get, - settings.jwtSecretKey, - settings.jwtLongevity, - settings.externalKnoraApiHostPort + appConfig.jwtSecretKey, + appConfig.jwtLongevityAsDuration, + appConfig.knoraApi.externalKnoraApiHostPort ) httpResponse = HttpResponse( headers = List( headers.`Set-Cookie`( HttpCookie( - calculateCookieName(settings), + calculateCookieName(appConfig), sessionToken, domain = cookieDomain, path = Some("/"), @@ -134,7 +133,7 @@ trait Authenticator extends InstrumentationSupport { * @return a [[HttpResponse]] containing either a failure message or a message with a cookie header containing * the generated session id. */ - def doLoginV2(credentials: KnoraPasswordCredentialsV2)(implicit + def doLoginV2(credentials: KnoraPasswordCredentialsV2, appConfig: AppConfig)(implicit system: ActorSystem, appActor: ActorRef, executionContext: ExecutionContext @@ -145,28 +144,27 @@ trait Authenticator extends InstrumentationSupport { for { // will throw exception if not valid and thus trigger the correct response _ <- authenticateCredentialsV2( - credentials = Some(credentials) + credentials = Some(credentials), + appConfig ) - settings = KnoraSettings(system) - userADM <- getUserByIdentifier( identifier = credentials.identifier ) - cookieDomain = Some(settings.cookieDomain) + cookieDomain = Some(appConfig.cookieDomain) token = JWTHelper.createToken( userADM.id, - settings.jwtSecretKey, - settings.jwtLongevity, - settings.externalKnoraApiHostPort + appConfig.jwtSecretKey, + appConfig.jwtLongevityAsDuration, + appConfig.knoraApi.externalKnoraApiHostPort ) httpResponse = HttpResponse( headers = List( headers.`Set-Cookie`( HttpCookie( - calculateCookieName(settings), + calculateCookieName(appConfig), token, domain = cookieDomain, path = Some("/"), @@ -187,12 +185,11 @@ trait Authenticator extends InstrumentationSupport { } def presentLoginFormV2( - requestContext: RequestContext + requestContext: RequestContext, + appConfig: AppConfig )(implicit system: ActorSystem, executionContext: ExecutionContext): Future[HttpResponse] = { - val settings = KnoraSettings(system) - - val apiUrl = settings.externalKnoraApiBaseUrl + val apiUrl = appConfig.knoraApi.externalKnoraApiBaseUrl val form = s""" @@ -246,19 +243,19 @@ trait Authenticator extends InstrumentationSupport { * @param system the current [[ActorSystem]] * @return a [[RequestContext]] */ - def doAuthenticateV1(requestContext: RequestContext)(implicit + def doAuthenticateV1(requestContext: RequestContext, appConfig: AppConfig)(implicit system: ActorSystem, appActor: ActorRef, executionContext: ExecutionContext ): Future[HttpResponse] = { - val settings = KnoraSettings(system) - val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, settings) + val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, appConfig) for { // will authenticate and either return or throw userADM: UserADM <- getUserADMThroughCredentialsV2( - credentials = credentials + credentials = credentials, + appConfig ) userProfile: UserProfileV1 = userADM.asUserProfileV1 @@ -284,19 +281,19 @@ trait Authenticator extends InstrumentationSupport { * @param system the current [[ActorSystem]] * @return a [[HttpResponse]] */ - def doAuthenticateV2(requestContext: RequestContext)(implicit + def doAuthenticateV2(requestContext: RequestContext, appConfig: AppConfig)(implicit system: ActorSystem, appActor: ActorRef, executionContext: ExecutionContext ): Future[HttpResponse] = { - val settings = KnoraSettings(system) - val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, settings) + val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, appConfig) for { // will throw exception if not valid _ <- authenticateCredentialsV2( - credentials = credentials + credentials = credentials, + appConfig ) httpResponse = HttpResponse( @@ -319,14 +316,14 @@ trait Authenticator extends InstrumentationSupport { * Used to logout the user, i.e. returns a header deleting the cookie and puts the token on the 'invalidated' list. * * @param requestContext a [[RequestContext]] containing the http request + * @param appConfig the application's configuration * @param system the current [[ActorSystem]] * @return a [[HttpResponse]] */ - def doLogoutV2(requestContext: RequestContext)(implicit system: ActorSystem): HttpResponse = { + def doLogoutV2(requestContext: RequestContext, appConfig: AppConfig)(implicit system: ActorSystem): HttpResponse = { - val settings = KnoraSettings(system) - val credentials = extractCredentialsV2(requestContext, settings) - val cookieDomain = Some(settings.cookieDomain) + val credentials = extractCredentialsV2(requestContext, appConfig) + val cookieDomain = Some(appConfig.cookieDomain) credentials match { case Some(KnoraSessionCredentialsV2(sessionToken)) => @@ -336,7 +333,7 @@ trait Authenticator extends InstrumentationSupport { headers = List( headers.`Set-Cookie`( HttpCookie( - calculateCookieName(settings), + calculateCookieName(appConfig), "", domain = cookieDomain, path = Some("/"), @@ -362,7 +359,7 @@ trait Authenticator extends InstrumentationSupport { headers = List( headers.`Set-Cookie`( HttpCookie( - calculateCookieName(settings), + calculateCookieName(appConfig), "", domain = cookieDomain, path = Some("/"), @@ -410,14 +407,13 @@ trait Authenticator extends InstrumentationSupport { * @param system the current [[ActorSystem]] * @return a [[UserProfileV1]] */ - def getUserADM(requestContext: RequestContext)(implicit + def getUserADM(requestContext: RequestContext, appConfig: AppConfig)(implicit system: ActorSystem, appActor: ActorRef, executionContext: ExecutionContext ): Future[UserADM] = { - val settings = KnoraSettings(system) - val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, settings) + val credentials: Option[KnoraCredentialsV2] = extractCredentialsV2(requestContext, appConfig) if (credentials.isEmpty) { log.debug("getUserADM - No credentials found, returning 'anonymousUser'.") @@ -426,7 +422,8 @@ trait Authenticator extends InstrumentationSupport { for { user: UserADM <- getUserADMThroughCredentialsV2( - credentials = credentials + credentials = credentials, + appConfig ) _ = log.debug("Authenticator - getUserADM - user: {}", user) @@ -473,15 +470,14 @@ object Authenticator extends InstrumentationSupport { * when the password does not match; when the supplied token is not valid. */ def authenticateCredentialsV2( - credentials: Option[KnoraCredentialsV2] + credentials: Option[KnoraCredentialsV2], + appConfig: AppConfig )(implicit system: ActorSystem, appActor: ActorRef, executionContext: ExecutionContext ): Future[Boolean] = for { - settings <- FastFuture.successful(KnoraSettings(system)) - result <- credentials match { case Some(passCreds: KnoraPasswordCredentialsV2) => for { @@ -501,14 +497,24 @@ object Authenticator extends InstrumentationSupport { } } yield true case Some(KnoraJWTTokenCredentialsV2(jwtToken)) => - if (!JWTHelper.validateToken(jwtToken, settings.jwtSecretKey, settings.externalKnoraApiHostPort)) { + if ( + !JWTHelper.validateToken( + jwtToken, + appConfig.jwtSecretKey, + appConfig.knoraApi.externalKnoraApiHostPort + ) + ) { log.debug("authenticateCredentialsV2 - token was not valid") throw BadCredentialsException(BAD_CRED_NOT_VALID) } FastFuture.successful(true) case Some(KnoraSessionCredentialsV2(sessionToken)) => if ( - !JWTHelper.validateToken(sessionToken, settings.jwtSecretKey, settings.externalKnoraApiHostPort) + !JWTHelper.validateToken( + sessionToken, + appConfig.jwtSecretKey, + appConfig.knoraApi.externalKnoraApiHostPort + ) ) { log.debug("authenticateCredentialsV2 - session token was not valid") throw BadCredentialsException(BAD_CRED_NOT_VALID) @@ -533,14 +539,14 @@ object Authenticator extends InstrumentationSupport { */ private def extractCredentialsV2( requestContext: RequestContext, - settings: KnoraSettingsImpl + appConfig: AppConfig ): Option[KnoraCredentialsV2] = { // log.debug("extractCredentialsV2 start ...") val credentialsFromParameters: Option[KnoraCredentialsV2] = extractCredentialsFromParametersV2(requestContext) log.debug("extractCredentialsV2 - credentialsFromParameters: {}", credentialsFromParameters) - val credentialsFromHeaders: Option[KnoraCredentialsV2] = extractCredentialsFromHeaderV2(requestContext, settings) + val credentialsFromHeaders: Option[KnoraCredentialsV2] = extractCredentialsFromHeaderV2(requestContext, appConfig) log.debug("extractCredentialsV2 - credentialsFromHeader: {}", credentialsFromHeaders) // return found credentials based on precedence: 1. url parameters, 2. header (basic auth, token) @@ -616,19 +622,19 @@ object Authenticator extends InstrumentationSupport { * 2. authorization token * 3. session token * - * @param requestContext the HTTP request context. - * @param settings the application settings. + * @param requestContext the HTTP request context. + * @param appConfig the application's configuration. * @return an optional [[KnoraCredentialsV2]]. */ private def extractCredentialsFromHeaderV2( requestContext: RequestContext, - settings: KnoraSettingsImpl + appConfig: AppConfig ): Option[KnoraCredentialsV2] = { // Session token from cookie header val cookies: Seq[HttpCookiePair] = requestContext.request.cookies val maybeSessionCreds: Option[KnoraSessionCredentialsV2] = - cookies.find(_.name == calculateCookieName(settings)) match { + cookies.find(_.name == calculateCookieName(appConfig)) match { case Some(authCookie) => val value: String = authCookie.value Some(KnoraSessionCredentialsV2(value)) @@ -704,17 +710,15 @@ object Authenticator extends InstrumentationSupport { * @throws AuthenticationException when the IRI can not be found inside the token, which is probably a bug. */ private def getUserADMThroughCredentialsV2( - credentials: Option[KnoraCredentialsV2] + credentials: Option[KnoraCredentialsV2], + appConfig: AppConfig )(implicit system: ActorSystem, appActor: ActorRef, executionContext: ExecutionContext - ): Future[UserADM] = { - - val settings = KnoraSettings(system) - + ): Future[UserADM] = for { - _ <- authenticateCredentialsV2(credentials) + _ <- authenticateCredentialsV2(credentials, appConfig) user <- credentials match { case Some(passCreds: KnoraPasswordCredentialsV2) => @@ -724,8 +728,8 @@ object Authenticator extends InstrumentationSupport { case Some(KnoraJWTTokenCredentialsV2(jwtToken)) => val userIri: IRI = JWTHelper.extractUserIriFromToken( jwtToken, - settings.jwtSecretKey, - settings.externalKnoraApiHostPort + appConfig.jwtSecretKey, + appConfig.knoraApi.externalKnoraApiHostPort ) match { case Some(iri) => iri case None => @@ -740,8 +744,8 @@ object Authenticator extends InstrumentationSupport { case Some(KnoraSessionCredentialsV2(sessionToken)) => val userIri: IRI = JWTHelper.extractUserIriFromToken( sessionToken, - settings.jwtSecretKey, - settings.externalKnoraApiHostPort + appConfig.jwtSecretKey, + appConfig.knoraApi.externalKnoraApiHostPort ) match { case Some(iri) => iri case None => @@ -758,7 +762,6 @@ object Authenticator extends InstrumentationSupport { } } yield user - } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // TRIPLE STORE ACCESS @@ -809,12 +812,12 @@ object Authenticator extends InstrumentationSupport { * The default padding needs to be changed from '=' to '9' because '=' is not allowed inside the cookie!!! * This also needs to be changed in all the places that base32 is used to calculate the cookie name, e.g., sipi. * - * @param settings the application settings. + * @param appConfig the application's configuration. */ - def calculateCookieName(settings: KnoraSettingsImpl): String = { + def calculateCookieName(appConfig: AppConfig): String = { // val base32 = new Base32('9'.toByte) - "KnoraAuthentication" + base32.encodeAsString(settings.externalKnoraApiHostPort.getBytes()) + "KnoraAuthentication" + base32.encodeAsString(appConfig.knoraApi.externalKnoraApiHostPort.getBytes()) } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala index c4170cea43..9317aca03a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala @@ -13,6 +13,7 @@ import spray.json.JsObject import spray.json.JsString import zio._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.core.State import org.knora.webapi.core.domain.AppState import org.knora.webapi.messages.util.KnoraSystemInstances @@ -114,7 +115,7 @@ trait HealthCheck { /** * Provides the '/health' endpoint serving the health status. */ -final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State]) +final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State], appConfig: AppConfig) extends HealthCheck with Authenticator { @@ -129,9 +130,10 @@ final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State]) _ <- ZIO.logInfo("health route start") ec <- ZIO.executor.map(_.asExecutionContext) state <- ZIO.service[State] - requestingUser <- ZIO - .fromFuture(_ => getUserADM(requestContext)(routeData.system, routeData.appActor, ec)) - .orElse(ZIO.succeed(KnoraSystemInstances.Users.AnonymousUser)) + requestingUser <- + ZIO + .fromFuture(_ => getUserADM(requestContext, appConfig)(routeData.system, routeData.appActor, ec)) + .orElse(ZIO.succeed(KnoraSystemInstances.Users.AnonymousUser)) result <- healthCheck(state) _ <- ZIO.logInfo("health route finished") @@ ZIOAspect.annotated("user-id", requestingUser.id.toString()) } yield result diff --git a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala index 48fb6fbcf9..3246ff6bf9 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala @@ -9,7 +9,6 @@ import akka.actor.ActorRef import akka.actor.ActorSystem import akka.http.scaladsl.server.Route import akka.pattern._ -import akka.stream.Materializer import akka.util.Timeout import com.typesafe.scalalogging.Logger import zio.prelude.Validation @@ -19,14 +18,13 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter 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 import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl /** * Data needed to be passed to each route. @@ -34,24 +32,21 @@ import org.knora.webapi.settings.KnoraSettingsImpl * @param system the actor system. * @param appActor the main application actor. */ -case class KnoraRouteData(system: akka.actor.ActorSystem, appActor: akka.actor.ActorRef) +case class KnoraRouteData(system: akka.actor.ActorSystem, appActor: akka.actor.ActorRef, appConfig: AppConfig) /** * An abstract class providing functionality that is commonly used in implementing Knora routes. * * @param routeData a [[KnoraRouteData]] providing access to the application. */ -abstract class KnoraRoute(routeData: KnoraRouteData) { +abstract class KnoraRoute(routeData: KnoraRouteData, appConfig: AppConfig) { implicit protected val system: ActorSystem = routeData.system - implicit protected val settings: KnoraSettingsImpl = KnoraSettings(system) - implicit protected val timeout: Timeout = settings.defaultTimeout + implicit protected val timeout: Timeout = appConfig.defaultTimeoutAsDuration implicit protected val executionContext: ExecutionContext = system.dispatcher - implicit protected val materializer: Materializer = Materializer.matFromSystem(system) implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance implicit protected val appActor: ActorRef = routeData.appActor protected val log: Logger = Logger(this.getClass) - protected val baseApiUrl: String = settings.internalKnoraApiBaseUrl /** * Constructs a route. @@ -64,7 +59,6 @@ abstract class KnoraRoute(routeData: KnoraRouteData) { * Gets a [[ProjectADM]] corresponding to the specified project IRI. * * @param projectIri the project IRI. - * * @param requestingUser the user making the request. * @return the corresponding [[ProjectADM]]. */ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala index 5bd9b999b3..537082c6f6 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala @@ -15,9 +15,9 @@ import scala.concurrent.Future import scala.util.Failure import scala.util.Success +import org.knora.webapi.config.AppConfig import org.knora.webapi.core.State import org.knora.webapi.core.domain.AppState -import org.knora.webapi.settings.KnoraSettings /** * A route used for rejecting requests to certain paths depending on the state of the app or the configuration. @@ -28,9 +28,8 @@ import org.knora.webapi.settings.KnoraSettings * * TODO: This should probably be refactored into a ZIO-HTTP middleware, when the transistion to ZIO-HTTP is done. */ -class RejectingRoute(system: akka.actor.ActorSystem, runtime: Runtime[State]) { self => +class RejectingRoute(system: akka.actor.ActorSystem, runtime: Runtime[State], appConfig: AppConfig) { self => - val settings = KnoraSettings(system) val log: Logger = Logger(this.getClass) /** @@ -53,7 +52,7 @@ class RejectingRoute(system: akka.actor.ActorSystem, runtime: Runtime[State]) { def makeRoute: Route = path(Remaining) { wholePath => // check to see if route is on the rejection list - val rejectSeq: Seq[Option[Boolean]] = settings.routesToReject.map { pathToReject: String => + val rejectSeq: Seq[Option[Boolean]] = appConfig.routesToReject.map { pathToReject: String => if (wholePath.contains(pathToReject.toCharArray)) { Some(true) } else { diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilADM.scala index 3ee82af4db..3fbf0c16cc 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilADM.scala @@ -19,7 +19,6 @@ import scala.concurrent.Future import dsp.errors.UnexpectedMessageException import org.knora.webapi.messages.ResponderRequest.KnoraRequestADM import org.knora.webapi.messages.admin.responder.KnoraResponseADM -import org.knora.webapi.settings.KnoraSettingsImpl /** * Convenience methods for Knora Admin routes. @@ -32,7 +31,6 @@ object RouteUtilADM { * @param requestMessageF a future containing a [[KnoraRequestADM]] message that should be sent to the responder manager. * @param requestContext the akka-http [[RequestContext]]. * - * @param settings the application's settings. * @param appActor a reference to the application actor. * @param log a logging adapter. * @param timeout a timeout for `ask` messages. @@ -42,7 +40,6 @@ object RouteUtilADM { def runJsonRoute( requestMessageF: Future[KnoraRequestADM], requestContext: RequestContext, - settings: KnoraSettingsImpl, appActor: ActorRef, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[RouteResult] = { @@ -51,11 +48,6 @@ object RouteUtilADM { requestMessage <- requestMessageF - // Optionally log the request message. TODO: move this to the testing framework. - _ = if (settings.dumpMessages) { - log.debug(requestMessage.toString) - } - // Make sure the responder sent a reply of type KnoraResponseV2. knoraResponse <- (appActor.ask(requestMessage)).map { case replyMessage: KnoraResponseADM => replyMessage @@ -68,11 +60,6 @@ object RouteUtilADM { ) } - // Optionally log the reply message. TODO: move this to the testing framework. - _ = if (settings.dumpMessages) { - log.debug(knoraResponse.toString) - } - jsonResponse = knoraResponse.toJsValue.asJsObject } yield HttpResponse( status = StatusCodes.OK, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala index 301318ca38..5920892aab 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV1.scala @@ -38,7 +38,6 @@ import org.knora.webapi.messages.v1.responder.valuemessages.StillImageFileValueV import org.knora.webapi.messages.v1.responder.valuemessages.TextFileValueV1 import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingRequestV2 import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingResponseV2 -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.store.iiif.errors.SipiException /** @@ -51,8 +50,7 @@ object RouteUtilV1 { * * @param requestMessage a [[KnoraRequestV1]] message that should be sent to the responder manager. * @param requestContext the akka-http [[RequestContext]]. - * @param settings the application's settings. - * @param responderManager a reference to the responder manager. + * @param appActor a reference to the application actor. * @param log a logging adapter. * @param timeout a timeout for `ask` messages. * @param executionContext an execution context for futures. @@ -61,14 +59,9 @@ object RouteUtilV1 { def runJsonRoute( requestMessage: KnoraRequestV1, requestContext: RequestContext, - settings: KnoraSettingsImpl, appActor: ActorRef, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[RouteResult] = { - // Optionally log the request message. TODO: move this to the testing framework. - if (settings.dumpMessages) { - log.debug(requestMessage.toString) - } val httpResponse: Future[HttpResponse] = for { // Make sure the responder sent a reply of type KnoraResponseV1. @@ -83,11 +76,6 @@ object RouteUtilV1 { ) } - // Optionally log the reply message. TODO: move this to the testing framework. - _ = if (settings.dumpMessages) { - log.debug(knoraResponse.toString) - } - // The request was successful, so add a status of ApiStatusCodesV1.OK to the response. jsonResponseWithStatus = JsObject( @@ -110,8 +98,7 @@ object RouteUtilV1 { * * @param requestMessageF a [[Future]] containing a [[KnoraRequestV1]] message that should be sent to the responder manager. * @param requestContext the akka-http [[RequestContext]]. - * @param settings the application's settings. - * @param responderManager a reference to the responder manager. + * @param appActor a reference to the application actor. * @param log a logging adapter. * @param timeout a timeout for `ask` messages. * @param executionContext an execution context for futures. @@ -120,7 +107,6 @@ object RouteUtilV1 { def runJsonRouteWithFuture[RequestMessageT <: KnoraRequestV1]( requestMessageF: Future[RequestMessageT], requestContext: RequestContext, - settings: KnoraSettingsImpl, appActor: ActorRef, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[RouteResult] = @@ -129,7 +115,6 @@ object RouteUtilV1 { routeResult <- runJsonRoute( requestMessage = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -143,7 +128,6 @@ object RouteUtilV1 { * @param requestMessageF a [[Future]] containing the message that should be sent to the responder manager. * @param viewHandler a function that can generate HTML from the responder's reply message. * @param requestContext the [[RequestContext]]. - * @param settings the application's settings. * @param responderManager a reference to the responder manager. * @param log a logging adapter. * @param timeout a timeout for `ask` messages. @@ -153,7 +137,6 @@ object RouteUtilV1 { requestMessageF: Future[RequestMessageT], viewHandler: (ReplyMessageT, ActorRef) => String, requestContext: RequestContext, - settings: KnoraSettingsImpl, appActor: ActorRef, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[RouteResult] = { @@ -162,11 +145,6 @@ object RouteUtilV1 { requestMessage <- requestMessageF - // Optionally log the request message. TODO: move this to the testing framework. - _ = if (settings.dumpMessages) { - log.debug(requestMessage.toString) - } - // Make sure the responder sent a reply of type ReplyMessageT. knoraResponse <- (appActor.ask(requestMessage)).map { case replyMessage: ReplyMessageT => replyMessage @@ -178,11 +156,6 @@ object RouteUtilV1 { throw UnexpectedMessageException(msg) } - // Optionally log the reply message. TODO: move this to the testing framework. - _ = if (settings.dumpMessages) { - log.debug(knoraResponse.toString) - } - } yield HttpResponse( status = StatusCodes.OK, entity = HttpEntity( @@ -203,8 +176,6 @@ object RouteUtilV1 { * resources. In a bulk import, this allows standoff links to resources * that are to be created by the import. * @param userProfile the user making the request. - * - * @param settings the application's settings. * @param responderManager a reference to the responder manager. * @param log a logging adapter. * @param timeout a timeout for `ask` messages. @@ -216,7 +187,6 @@ object RouteUtilV1 { mappingIri: IRI, acceptStandoffLinksToClientIDs: Boolean, userProfile: UserADM, - settings: KnoraSettingsImpl, appActor: ActorRef, log: Logger )(implicit timeout: Timeout, executionContext: ExecutionContext): Future[TextWithStandoffTagsV2] = 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 2e404c3dc4..89ed8990d2 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala @@ -20,6 +20,7 @@ import scala.util.control.Exception.catching import dsp.errors.BadRequestException import dsp.errors.UnexpectedMessageException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 import org.knora.webapi.messages.SmartIri @@ -30,7 +31,6 @@ import org.knora.webapi.messages.util.rdf.RdfFormat import org.knora.webapi.messages.util.rdf.RdfModel import org.knora.webapi.messages.v2.responder.KnoraResponseV2 import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceTEIGetResponseV2 -import org.knora.webapi.settings.KnoraSettingsImpl /** * Handles message formatting, content negotiation, and simple interactions with responders, on behalf of Knora routes. @@ -203,10 +203,11 @@ object RouteUtilV2 { * * @param requestMessage a future containing a [[KnoraRequestV2]] message that should be sent to the responder manager. * @param requestContext the akka-http [[RequestContext]]. - * @param settings the application's settings. - * @param responderManager a reference to the responder manager. + * @param appConfig the application's configuration + * @param appActor a reference to the application actor. * @param log a logging adapter. * @param targetSchema the API schema that should be used in the response. + * @param schemaOptions the schema options that should be used when processing the request. * @param timeout a timeout for `ask` messages. * @param executionContext an execution context for futures. * @return a [[Future]] containing a [[RouteResult]]. @@ -214,7 +215,7 @@ object RouteUtilV2 { private def runRdfRoute( requestMessage: KnoraRequestV2, requestContext: RequestContext, - settings: KnoraSettingsImpl, + appConfig: AppConfig, appActor: ActorRef, log: Logger, targetSchema: OntologySchema, @@ -247,7 +248,7 @@ object RouteUtilV2 { formattedResponseContent: String = knoraResponse.format( rdfFormat = RdfFormat.fromMediaType(specificMediaType), targetSchema = targetSchema, - settings = settings, + appConfig = appConfig, schemaOptions = schemaOptions ) } yield HttpResponse( @@ -314,8 +315,8 @@ object RouteUtilV2 { * * @param requestMessageF a [[Future]] containing a [[KnoraRequestV2]] message that should be sent to the responder manager. * @param requestContext the akka-http [[RequestContext]]. - * @param settings the application's settings. - * @param responderManager a reference to the responder manager. + * @param appConfig the application's configuration + * @param appActor a reference to the application actor. * @param log a logging adapter. * @param targetSchema the API schema that should be used in the response. * @param schemaOptions the schema options that should be used when processing the request. @@ -326,7 +327,7 @@ object RouteUtilV2 { def runRdfRouteWithFuture( requestMessageF: Future[KnoraRequestV2], requestContext: RequestContext, - settings: KnoraSettingsImpl, + appConfig: AppConfig, appActor: ActorRef, log: Logger, targetSchema: OntologySchema, @@ -337,7 +338,7 @@ object RouteUtilV2 { routeResult <- runRdfRoute( requestMessage = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala deleted file mode 100644 index cb0bcbf939..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 akka.http.scaladsl.server.Route -import com.github.swagger.akka.SwaggerHttpService -import com.github.swagger.akka.model.Info -import io.swagger.models.ExternalDocs -import io.swagger.models.Scheme -import io.swagger.models.auth.BasicAuthDefinition - -import org.knora.webapi.routing.admin._ -import org.knora.webapi.routing.admin.lists._ - -/** - * Provides the '/api-docs' endpoint serving the 'swagger.json' OpenAPI specification - */ -class SwaggerApiDocsRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) with SwaggerHttpService { - - // List all routes here - override val apiClasses: Set[Class[_]] = Set( - classOf[GroupsRouteADM], - classOf[DeleteListItemsRouteADM], - classOf[CreateListItemsRouteADM], - classOf[GetListItemsRouteADM], - classOf[UpdateListItemsRouteADM], - classOf[PermissionsRouteADM], - classOf[ProjectsRouteADM], - classOf[StoreRouteADM], - classOf[UsersRouteADM], - classOf[HealthRoute] - ) - - override val schemes: List[Scheme] = if (settings.externalKnoraApiProtocol == "http") { - List(Scheme.HTTP) - } else if (settings.externalKnoraApiProtocol == "https") { - List(Scheme.HTTPS) - } else { - List(Scheme.HTTP) - } - - // swagger will publish at: http://locahost:3333/api-docs/swagger.json - - override val host: String = settings.externalKnoraApiHostPort // the url of your api, not swagger's json endpoint - override val basePath = "/" // the basePath for the API you are exposing - override val apiDocsPath = "api-docs" // where you want the swagger-json endpoint exposed - override val info: Info = Info(version = "1.8.0") // provides license and other description details - override val externalDocs: Option[ExternalDocs] = Some(new ExternalDocs("Knora Docs", "http://docs.knora.org")) - override val securitySchemeDefinitions = Map("basicAuth" -> new BasicAuthDefinition()) - - /** - * Returns the route. - */ - override def makeRoute: Route = - routes - -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala index b87bfd4e01..b2abcb9159 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala @@ -9,6 +9,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.sipimessages.SipiFileInfoGetRequestADM import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -18,7 +19,9 @@ import org.knora.webapi.routing.RouteUtilADM /** * Provides a routing function for the API that Sipi connects to. */ -class FilesRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class FilesRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * A routing function for the API that Sipi connects to. @@ -30,7 +33,7 @@ class FilesRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit path("admin" / "files" / Segments(2)) { projectIDAndFile: Seq[String] => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) projectID = stringFormatter.validateProjectShortcode( projectIDAndFile.head, throw BadRequestException(s"Invalid project ID: '${projectIDAndFile.head}'") @@ -49,7 +52,6 @@ class FilesRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala index b57fc60533..77ead96303 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala @@ -17,6 +17,7 @@ import javax.ws.rs.Path import dsp.errors.BadRequestException import dsp.valueobjects.Group._ import dsp.valueobjects.Iri._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.groupsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -29,8 +30,8 @@ import org.knora.webapi.routing.RouteUtilADM @Api(value = "groups", produces = "application/json") @Path("/admin/groups") -class GroupsRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with GroupsADMJsonProtocol { @@ -51,13 +52,12 @@ class GroupsRouteADM(routeData: KnoraRouteData) private def getGroups(): Route = path(groupsBasePath) { get { requestContext => val requestMessage = for { - _ <- getUserADM(requestContext) + _ <- getUserADM(requestContext, appConfig) } yield GroupsGetRequestADM() RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -73,7 +73,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid custom group IRI $value")) val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GroupGetRequestADM( groupIri = checkedGroupIri, requestingUser = requestingUser @@ -82,7 +82,6 @@ class GroupsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -99,7 +98,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid group IRI $value")) val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GroupMembersGetRequestADM( groupIri = checkedGroupIri, requestingUser = requestingUser @@ -108,7 +107,6 @@ class GroupsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -133,7 +131,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) val requestMessage = for { payload <- toFuture(validatedGroupCreatePayload) - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GroupCreateRequestADM( createRequest = payload, requestingUser = requestingUser, @@ -143,7 +141,6 @@ class GroupsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -181,7 +178,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) val requestMessage = for { payload <- toFuture(validatedGroupUpdatePayload) - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GroupChangeRequestADM( groupIri = checkedGroupIri, changeGroupRequest = payload, @@ -192,7 +189,6 @@ class GroupsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -222,7 +218,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) } val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GroupChangeStatusRequestADM( groupIri = checkedGroupIri, changeGroupRequest = apiRequest, @@ -233,7 +229,6 @@ class GroupsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -250,7 +245,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid group IRI $value")) val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GroupChangeStatusRequestADM( groupIri = checkedGroupIri, changeGroupRequest = ChangeGroupApiRequestADM(status = Some(false)), @@ -261,7 +256,6 @@ class GroupsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala index 0a8e5e9495..5513df2037 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala @@ -8,6 +8,7 @@ package org.knora.webapi.routing.admin import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.admin.lists._ @@ -15,11 +16,11 @@ import org.knora.webapi.routing.admin.lists._ /** * Provides an akka-http-routing function for API routes that deal with lists. */ -class ListsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) { - private val getNodeRoute: GetListItemsRouteADM = new GetListItemsRouteADM(routeData) - private val createNodeRoute: CreateListItemsRouteADM = new CreateListItemsRouteADM(routeData) - private val deleteNodeRoute: DeleteListItemsRouteADM = new DeleteListItemsRouteADM(routeData) - private val updateNodeRoute: UpdateListItemsRouteADM = new UpdateListItemsRouteADM(routeData) +class ListsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) extends KnoraRoute(routeData, appConfig) { + private val getNodeRoute: GetListItemsRouteADM = new GetListItemsRouteADM(routeData, appConfig) + private val createNodeRoute: CreateListItemsRouteADM = new CreateListItemsRouteADM(routeData, appConfig) + private val deleteNodeRoute: DeleteListItemsRouteADM = new DeleteListItemsRouteADM(routeData, appConfig) + private val updateNodeRoute: UpdateListItemsRouteADM = new UpdateListItemsRouteADM(routeData, appConfig) override def makeRoute: Route = getNodeRoute.makeRoute ~ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala index 37fc7edbd2..39852b42d9 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala @@ -8,6 +8,7 @@ package org.knora.webapi.routing.admin import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.admin.permissions._ @@ -15,11 +16,11 @@ import org.knora.webapi.routing.admin.permissions._ /** * Provides an akka-http-routing function for API routes that deal with permissions. */ -class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) { - private val createPermissionRoute: CreatePermissionRouteADM = new CreatePermissionRouteADM(routeData) - private val getPermissionRoute: GetPermissionsRouteADM = new GetPermissionsRouteADM(routeData) - private val updatePermissionRoute: UpdatePermissionRouteADM = new UpdatePermissionRouteADM(routeData) - private val deletePermissionRoute: DeletePermissionRouteADM = new DeletePermissionRouteADM(routeData) +class PermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) extends KnoraRoute(routeData, appConfig) { + private val createPermissionRoute: CreatePermissionRouteADM = new CreatePermissionRouteADM(routeData, appConfig) + private val getPermissionRoute: GetPermissionsRouteADM = new GetPermissionsRouteADM(routeData, appConfig) + private val updatePermissionRoute: UpdatePermissionRouteADM = new UpdatePermissionRouteADM(routeData, appConfig) + private val deletePermissionRoute: DeletePermissionRouteADM = new DeletePermissionRouteADM(routeData, appConfig) override def makeRoute: Route = createPermissionRoute.makeRoute ~ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala index 9af61fcba1..00630e0c8f 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala @@ -32,16 +32,16 @@ import dsp.valueobjects.Iri.ProjectIri import dsp.valueobjects.Project._ import org.knora.webapi.IRI import org.knora.webapi.annotation.ApiMayChange +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM - @Api(value = "projects", produces = "application/json") @Path("/admin/projects") -class ProjectsRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with ProjectsADMJsonProtocol { @@ -88,7 +88,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) log.info("All projects requested.") val requestMessage: Future[ProjectsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectsGetRequestADM( requestingUser = requestingUser @@ -97,7 +98,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -147,7 +147,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ProjectCreateRequestADM] = for { projectCreatePayload <- toFuture(projectCreatePayload) - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield ProjectCreateRequestADM( createRequest = projectCreatePayload, requestingUser = requestingUser, @@ -157,7 +157,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -170,7 +169,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectsKeywordsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectsKeywordsGetRequestADM( requestingUser = requestingUser @@ -179,7 +179,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -195,7 +194,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ProjectKeywordsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectKeywordsGetRequestADM( projectIri = checkedProjectIri, @@ -205,7 +205,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -220,7 +219,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -233,7 +233,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -248,7 +247,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) shortNameDec = stringFormatter.validateAndEscapeProjectShortname( value, @@ -263,7 +263,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -278,7 +277,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) checkedShortcode = stringFormatter.validateAndEscapeProjectShortcode( value, @@ -293,7 +293,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -314,7 +313,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ProjectChangeRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectChangeRequestADM( projectIri = checkedProjectIri, @@ -326,7 +326,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -346,7 +345,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ProjectChangeRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectChangeRequestADM( projectIri = checkedProjectIri, @@ -358,7 +358,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -374,7 +373,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -387,7 +387,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -403,7 +402,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) shortNameDec = stringFormatter.validateAndEscapeProjectShortname( value, @@ -418,7 +418,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -434,7 +433,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) checkedShortcode = stringFormatter.validateAndEscapeProjectShortcode( value, @@ -449,7 +449,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -465,7 +464,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -478,7 +478,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -494,7 +493,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) checkedShortname = stringFormatter.validateAndEscapeProjectShortname( value, @@ -509,7 +509,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -525,7 +524,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) checkedShortcode = stringFormatter.validateProjectShortcode( value, @@ -540,7 +540,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -556,7 +555,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectRestrictedViewSettingsGetRequestADM( @@ -567,7 +567,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -583,7 +582,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) shortNameDec = java.net.URLDecoder.decode(value, "utf-8") @@ -595,7 +595,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -611,7 +610,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectRestrictedViewSettingsGetRequestADM( identifier = ProjectIdentifierADM(maybeShortcode = Some(value)), @@ -621,7 +621,6 @@ class ProjectsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -650,7 +649,8 @@ class ProjectsRouteADM(routeData: KnoraRouteData) val httpEntityFuture: Future[HttpEntity.Chunked] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage = ProjectDataGetRequestADM( diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala index f7fda2646e..dfca5ebcfd 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala @@ -13,6 +13,7 @@ import javax.ws.rs.Path import scala.concurrent.Future import scala.concurrent.duration._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.storesmessages.ResetTriplestoreContentRequestADM import org.knora.webapi.messages.admin.responder.storesmessages.StoresADMJsonProtocol import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject @@ -27,8 +28,8 @@ import org.knora.webapi.routing.RouteUtilADM @Api(value = "store", produces = "application/json") @Path("/admin/store") -class StoreRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class StoreRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with StoresADMJsonProtocol { @@ -60,7 +61,6 @@ class StoreRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log )(timeout = 479999.milliseconds, executionContext = executionContext) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala index 3c7cef3855..d3d827f65d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala @@ -20,6 +20,7 @@ import dsp.valueobjects.Iri.UserIri import dsp.valueobjects.LanguageCode import dsp.valueobjects.User._ import org.knora.webapi.annotation.ApiMayChange +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.usersmessages.UsersADMJsonProtocol._ import org.knora.webapi.messages.admin.responder.usersmessages._ import org.knora.webapi.messages.util.KnoraSystemInstances @@ -33,7 +34,9 @@ import org.knora.webapi.routing.RouteUtilADM */ @Api(value = "users", produces = "application/json") @Path("/admin/users") -class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { val usersBasePath: PathMatcher[Unit] = PathMatcher("admin" / "users") @@ -71,7 +74,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit get { requestContext => val requestMessage: Future[UsersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UsersGetRequestADM( requestingUser = requestingUser @@ -80,7 +84,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -143,7 +146,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserCreateRequestADM] = for { payload <- toFuture(validatedUserCreatePayload) - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield UserCreateRequestADM( userCreatePayloadADM = payload, requestingUser = requestingUser, @@ -153,7 +156,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -168,7 +170,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserGetRequestADM( identifier = UserIdentifierADM(maybeIri = Some(userIri)), @@ -179,7 +182,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -194,7 +196,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserGetRequestADM( identifier = UserIdentifierADM(maybeEmail = Some(userIri)), @@ -205,7 +208,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -220,7 +222,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserGetRequestADM( identifier = UserIdentifierADM(maybeUsername = Some(userIri)), @@ -231,7 +234,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -296,7 +298,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /* the api request is already checked at time of creation. see case class. */ val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserChangeBasicInformationRequestADM( userIri = checkedUserIri, @@ -308,7 +311,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -348,7 +350,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserChangePasswordRequestADM( userIri = checkedUserIri, @@ -360,7 +363,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -396,7 +398,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserChangeStatusRequestADM( userIri = checkedUserIri, @@ -408,7 +411,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -440,7 +442,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserChangeStatusRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserChangeStatusRequestADM( userIri = checkedUserIri, @@ -452,7 +455,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -487,7 +489,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserChangeSystemAdminMembershipStatusRequestADM( userIri = checkedUserIri, @@ -499,7 +502,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -521,7 +523,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserProjectMembershipsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserProjectMembershipsGetRequestADM( userIri = checkedUserIri, @@ -531,7 +534,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -566,7 +568,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserProjectMembershipAddRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserProjectMembershipAddRequestADM( userIri = checkedUserIri, @@ -578,7 +581,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -613,7 +615,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserProjectMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserProjectMembershipRemoveRequestADM( userIri = checkedUserIri, @@ -625,7 +628,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -646,7 +648,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserProjectAdminMembershipsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserProjectAdminMembershipsGetRequestADM( userIri = checkedUserIri, @@ -657,7 +660,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -692,7 +694,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserProjectAdminMembershipAddRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserProjectAdminMembershipAddRequestADM( userIri = checkedUserIri, @@ -704,7 +707,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -739,7 +741,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserProjectAdminMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserProjectAdminMembershipRemoveRequestADM( userIri = checkedUserIri, @@ -751,7 +754,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -772,7 +774,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserGroupMembershipsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserGroupMembershipsGetRequestADM( userIri = checkedUserIri, @@ -782,7 +785,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -814,7 +816,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserGroupMembershipAddRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserGroupMembershipAddRequestADM( userIri = checkedUserIri, @@ -826,7 +829,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -858,7 +860,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage: Future[UserGroupMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield UserGroupMembershipRemoveRequestADM( userIri = checkedUserIri, @@ -870,7 +873,6 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala index 60671706f5..6987ed9540 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala @@ -20,6 +20,7 @@ import dsp.errors.ForbiddenException import dsp.valueobjects.Iri._ import dsp.valueobjects.List._ import dsp.valueobjects.ListErrorMessages +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListRootNodeCreatePayloadADM import org.knora.webapi.messages.admin.responder.listsmessages._ @@ -35,8 +36,8 @@ import org.knora.webapi.routing.RouteUtilADM */ @Api(value = "lists", produces = "application/json") @Path("/admin/lists") -class CreateListItemsRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class CreateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with ListADMJsonProtocol { @@ -87,7 +88,7 @@ class CreateListItemsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ListRootNodeCreateRequestADM] = for { payload <- toFuture(validatedListRootNodeCreatePayload) - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) // check if the requesting user is allowed to perform operation _ = @@ -107,7 +108,6 @@ class CreateListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -165,7 +165,7 @@ class CreateListItemsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ListChildNodeCreateRequestADM] = for { payload <- toFuture(validatedCreateChildNodePeyload) - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) // check if the requesting user is allowed to perform operation _ = @@ -185,7 +185,6 @@ class CreateListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala index 2e068a7d05..630d103313 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala @@ -13,6 +13,7 @@ import java.util.UUID import scala.concurrent.Future import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -24,8 +25,8 @@ import org.knora.webapi.routing.RouteUtilADM * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ -class DeleteListItemsRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class DeleteListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with ListADMJsonProtocol { @@ -45,7 +46,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list item Iri: $iri")) val requestMessage: Future[ListItemDeleteRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield ListItemDeleteRequestADM( nodeIri = nodeIri, requestingUser = requestingUser, @@ -55,7 +56,6 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -72,7 +72,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list IRI: $iri")) val requestMessage: Future[CanDeleteListRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield CanDeleteListRequestADM( iri = listIri, requestingUser = requestingUser @@ -81,7 +81,6 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -98,7 +97,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ListNodeCommentsDeleteRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield ListNodeCommentsDeleteRequestADM( iri = listIri, requestingUser = requestingUser @@ -107,7 +106,6 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala index bed8f2f9e9..094b9487a1 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala @@ -15,6 +15,7 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -28,8 +29,8 @@ import org.knora.webapi.routing.RouteUtilADM */ @Api(value = "lists", produces = "application/json") @Path("/admin/lists") -class GetListItemsRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class GetListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with ListADMJsonProtocol { @@ -62,7 +63,8 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ListsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ListsGetRequestADM( projectIri = projectIri, @@ -72,7 +74,6 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -97,7 +98,8 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[ListGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ListGetRequestADM( iri = listIri, @@ -107,7 +109,6 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -123,7 +124,7 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) val requestMessage: Future[ListNodeInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield ListNodeInfoGetRequestADM( iri = listIri, requestingUser = requestingUser @@ -132,7 +133,6 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -150,7 +150,7 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) val requestMessage: Future[ListNodeInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield ListNodeInfoGetRequestADM( iri = listIri, requestingUser = requestingUser @@ -159,7 +159,6 @@ class GetListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala index 2fcda0fea9..41655a8517 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala @@ -20,6 +20,7 @@ import dsp.errors.ForbiddenException import dsp.valueobjects.Iri._ import dsp.valueobjects.List._ import dsp.valueobjects.ListErrorMessages +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -31,8 +32,8 @@ import org.knora.webapi.routing.RouteUtilADM * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ -class UpdateListItemsRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class UpdateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with ListADMJsonProtocol { @@ -82,7 +83,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) NodeNameChangePayloadADM(ListName.make(apiRequest.name).fold(e => throw e.head, v => v)) val requestMessage: Future[NodeNameChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield NodeNameChangeRequestADM( nodeIri = nodeIri, changeNodeNameRequest = namePayload, @@ -93,7 +94,6 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -138,7 +138,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) NodeLabelsChangePayloadADM(Labels.make(apiRequest.labels).fold(e => throw e.head, v => v)) val requestMessage: Future[NodeLabelsChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield NodeLabelsChangeRequestADM( nodeIri = nodeIri, changeNodeLabelsRequest = labelsPayload, @@ -149,7 +149,6 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -194,7 +193,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) NodeCommentsChangePayloadADM(Comments.make(apiRequest.comments).fold(e => throw e.head, v => v)) val requestMessage: Future[NodeCommentsChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield NodeCommentsChangeRequestADM( nodeIri = nodeIri, changeNodeCommentsRequest = commentsPayload, @@ -205,7 +204,6 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -247,7 +245,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param node IRI: $iri")) val requestMessage: Future[NodePositionChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield NodePositionChangeRequestADM( nodeIri = nodeIri, changeNodePositionRequest = apiRequest, @@ -258,7 +256,6 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -318,7 +315,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) val requestMessage: Future[NodeInfoChangeRequestADM] = for { payload <- toFuture(validatedChangeNodeInfoPayload) - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) // check if the requesting user is allowed to perform operation _ = if ( !requestingUser.permissions.isProjectAdmin( @@ -338,7 +335,6 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala index d144711181..f76fba616a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala @@ -14,16 +14,16 @@ import java.util.UUID import javax.ws.rs.Path import scala.concurrent.Future +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") -class CreatePermissionRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class CreatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with PermissionsADMJsonProtocol { @@ -45,7 +45,7 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData) /* create a new administrative permission */ entity(as[CreateAdministrativePermissionAPIRequestADM]) { apiRequest => requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield AdministrativePermissionCreateRequestADM( createRequest = apiRequest, requestingUser = requestingUser, @@ -55,7 +55,6 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -72,7 +71,7 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData) /* create a new default object access permission */ entity(as[CreateDefaultObjectAccessPermissionAPIRequestADM]) { apiRequest => requestContext => val requestMessage: Future[DefaultObjectAccessPermissionCreateRequestADM] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield DefaultObjectAccessPermissionCreateRequestADM( createRequest = apiRequest, requestingUser = requestingUser, @@ -82,7 +81,6 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala index f5c2dbc48f..c30a0f2881 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala @@ -13,16 +13,16 @@ import io.swagger.annotations._ import java.util.UUID import javax.ws.rs.Path +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") -class DeletePermissionRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class DeletePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with PermissionsADMJsonProtocol { @@ -41,7 +41,7 @@ class DeletePermissionRouteADM(routeData: KnoraRouteData) path(permissionsBasePath / Segment) { iri => delete { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield PermissionDeleteRequestADM( permissionIri = iri, requestingUser = requestingUser, @@ -51,7 +51,6 @@ class DeletePermissionRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala index 748d436d02..3aed5acbda 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala @@ -13,16 +13,16 @@ import io.swagger.annotations._ import java.util.UUID import javax.ws.rs.Path +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") -class GetPermissionsRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class GetPermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with PermissionsADMJsonProtocol { @@ -41,13 +41,12 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) path(permissionsBasePath / "ap" / Segment / Segment) { (projectIri, groupIri) => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield AdministrativePermissionForProjectGroupGetRequestADM(projectIri, groupIri, requestingUser) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -58,7 +57,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) path(permissionsBasePath / "ap" / Segment) { projectIri => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield AdministrativePermissionsForProjectGetRequestADM( projectIri = projectIri, requestingUser = requestingUser, @@ -68,7 +67,6 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -79,7 +77,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) path(permissionsBasePath / "doap" / Segment) { projectIri => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield DefaultObjectAccessPermissionsForProjectGetRequestADM( projectIri = projectIri, requestingUser = requestingUser, @@ -89,7 +87,6 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -100,7 +97,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) path(permissionsBasePath / Segment) { projectIri => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield PermissionsForProjectGetRequestADM( projectIri = projectIri, requestingUser = requestingUser, @@ -110,7 +107,6 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala index b8c40dbb38..23df1c0be5 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala @@ -14,16 +14,16 @@ import java.util.UUID import javax.ws.rs.Path import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") -class UpdatePermissionRouteADM(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class UpdatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with PermissionsADMJsonProtocol { @@ -49,7 +49,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield PermissionChangeGroupRequestADM( permissionIri = permissionIri, changePermissionGroupRequest = apiRequest, @@ -60,7 +60,6 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -79,7 +78,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield PermissionChangeHasPermissionsRequestADM( permissionIri = permissionIri, changePermissionHasPermissionsRequest = apiRequest, @@ -90,7 +89,6 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -109,7 +107,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield PermissionChangeResourceClassRequestADM( permissionIri = permissionIri, changePermissionResourceClassRequest = apiRequest, @@ -120,7 +118,6 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -139,7 +136,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield PermissionChangePropertyRequestADM( permissionIri = permissionIri, changePermissionPropertyRequest = apiRequest, @@ -150,7 +147,6 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala index a88a6cc238..dfebd12ca5 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala @@ -19,6 +19,7 @@ import java.io.ByteArrayOutputStream import java.nio.file.Paths import javax.imageio.ImageIO +import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData @@ -26,7 +27,9 @@ import org.knora.webapi.routing.KnoraRouteData /** * A route used for faking the image server. */ -class AssetsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class AssetsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala index 8dba65c2d3..70534a891a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala @@ -8,6 +8,7 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData @@ -15,7 +16,9 @@ import org.knora.webapi.routing.KnoraRouteData /** * A route providing authentication support. It allows the creation of "sessions", which is used in the SALSAH app. */ -class AuthenticationRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class AuthenticationRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -24,7 +27,7 @@ class AuthenticationRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeD path("v1" / "authenticate") { get { requestContext => requestContext.complete { - doAuthenticateV1(requestContext) + doAuthenticateV1(requestContext, appConfig) } } } ~ path("v1" / "session") { @@ -32,20 +35,20 @@ class AuthenticationRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeD requestContext.complete { val params = requestContext.request.uri.query().toMap if (params.contains("logout")) { - doLogoutV2(requestContext) + doLogoutV2(requestContext, appConfig) } else if (params.contains("login")) { - doLoginV1(requestContext) + doLoginV1(requestContext, appConfig) } else { - doAuthenticateV1(requestContext) + doAuthenticateV1(requestContext, appConfig) } } } ~ post { requestContext => requestContext.complete { - doLoginV1(requestContext) + doLoginV1(requestContext, appConfig) } } ~ delete { requestContext => requestContext.complete { - doLogoutV2(requestContext) + doLogoutV2(requestContext, appConfig) } } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala index f121d7f1a5..f558803b83 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala @@ -8,6 +8,7 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.ckanmessages.CkanRequestV1 import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -17,7 +18,9 @@ import org.knora.webapi.routing.RouteUtilV1 /** * A route used to serve data to CKAN. It is used be the Ckan instance running under http://data.humanities.ch. */ -class CkanRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class CkanRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -26,7 +29,7 @@ class CkanRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with path("v1" / "ckan") { get { requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext) + userProfile <- getUserADM(requestContext, appConfig) params = requestContext.request.uri.query().toMap project: Option[Seq[String]] = params.get("project").map(_.split(",").toSeq) limit: Option[Int] = params.get("limit").map(_.toInt) @@ -41,7 +44,6 @@ class CkanRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala index b0aa275cd9..c0c4a88972 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala @@ -9,6 +9,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.v1.responder.listmessages._ import org.knora.webapi.routing.Authenticator @@ -19,7 +20,9 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides API routes that deal with lists. */ -class ListsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class ListsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -31,7 +34,7 @@ class ListsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with path("v1" / "hlists" / Segment) { iri => get { requestContext => val requestMessageFuture = for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) listIri = stringFormatter.validateAndEscapeIri( iri, throw BadRequestException(s"Invalid param list IRI: $iri") @@ -47,7 +50,6 @@ class ListsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV1.runJsonRouteWithFuture( requestMessageFuture, requestContext, - settings, appActor, log ) @@ -56,7 +58,7 @@ class ListsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with path("v1" / "selections" / Segment) { iri => get { requestContext => val requestMessageFuture = for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) selIri = stringFormatter.validateAndEscapeIri( iri, throw BadRequestException(s"Invalid param list IRI: $iri") @@ -72,7 +74,6 @@ class ListsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV1.runJsonRouteWithFuture( requestMessageFuture, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala index 0b310034ab..100b865e31 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala @@ -9,14 +9,15 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.projectmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilV1 -class ProjectsRouteV1(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class ProjectsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with ProjectV1JsonProtocol { @@ -29,7 +30,7 @@ class ProjectsRouteV1(routeData: KnoraRouteData) /* returns all projects */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) } yield ProjectsGetRequestV1( userProfile = Some(userProfile) ) @@ -37,7 +38,6 @@ class ProjectsRouteV1(routeData: KnoraRouteData) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -49,7 +49,7 @@ class ProjectsRouteV1(routeData: KnoraRouteData) val requestMessage = if (identifier != "iri") { // identify project by shortname. val shortNameDec = java.net.URLDecoder.decode(value, "utf-8") for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) } yield ProjectInfoByShortnameGetRequestV1( shortname = shortNameDec, userProfileV1 = Some(userProfile) @@ -58,7 +58,7 @@ class ProjectsRouteV1(routeData: KnoraRouteData) val checkedProjectIri = stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid project IRI $value")) for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) } yield ProjectInfoByIRIGetRequestV1( iri = checkedProjectIri, userProfileV1 = Some(userProfile) @@ -68,7 +68,6 @@ class ProjectsRouteV1(routeData: KnoraRouteData) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala index 43415654f1..44c9921e7a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala @@ -9,6 +9,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.ontologymessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -18,7 +19,9 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides a spray-routing function for API routes that deal with resource types. */ -class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -28,7 +31,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa path("v1" / "resourcetypes" / Segment) { iri => get { requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext) + userProfile <- getUserADM(requestContext, appConfig) // TODO: Check that this is the IRI of a resource type and not just any IRI resourceTypeIri = @@ -42,7 +45,6 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -50,7 +52,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa } ~ path("v1" / "resourcetypes") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext) + userADM <- getUserADM(requestContext, appConfig) params = requestContext.request.uri.query().toMap vocabularyId = params.getOrElse( @@ -77,7 +79,6 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -86,7 +87,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa } ~ path("v1" / "propertylists") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext) + userADM <- getUserADM(requestContext, appConfig) params = requestContext.request.uri.query().toMap vocabularyId: Option[String] = params.get("vocabulary") @@ -131,7 +132,6 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -140,13 +140,12 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa } ~ path("v1" / "vocabularies") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext) + userADM <- getUserADM(requestContext, appConfig) } yield NamedGraphsGetRequestV1(userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -155,13 +154,12 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa } ~ path("v1" / "vocabularies" / "reload") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext) + userADM <- getUserADM(requestContext, appConfig) } yield LoadOntologiesRequestV1(userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -169,7 +167,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa } ~ path("v1" / "subclasses" / Segment) { iri => get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext) + userADM <- getUserADM(requestContext, appConfig) // TODO: Check that this is the IRI of a resource type and not just any IRI resourceClassIri = stringFormatter.validateAndEscapeIri( @@ -181,7 +179,6 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala index c0ab1d949c..7d8b6b9412 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala @@ -36,6 +36,7 @@ import dsp.errors.BadRequestException import dsp.errors.ForbiddenException import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -63,7 +64,9 @@ import org.knora.webapi.util.FileUtil /** * Provides API routes that deal with resources. */ -class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { // A scala.xml.PrettyPrinter for formatting generated XML import schemas. private val xmlPrettyPrinter = new scala.xml.PrettyPrinter(width = 160, step = 4) @@ -174,7 +177,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) acceptStandoffLinksToClientIDs = acceptStandoffLinksToClientIDs, userProfile = userProfile, - settings = settings, appActor = appActor, log = log ) @@ -327,7 +329,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) file: Option[FileValueV1] <- apiRequest.file match { case Some(filename) => // Ask Sipi about the file's metadata. - val tempFilePath = stringFormatter.makeSipiTempFilePath(settings, filename) + val tempFilePath = stringFormatter.makeSipiTempFilePath(filename) for { fileMetadataResponse: GetFileMetadataResponse <- @@ -391,7 +393,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) convertedFile <- resourceRequest.file match { case Some(filename) => // Ask Sipi about the file's metadata. - val tempFilePath = stringFormatter.makeSipiTempFilePath(settings, filename) + val tempFilePath = stringFormatter.makeSipiTempFilePath(filename) for { fileMetadataResponse: GetFileMetadataResponse <- appActor @@ -1216,7 +1218,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) requestContext => val requestMessage = for { userProfile <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) params = requestContext.request.uri.query().toMap searchstr = params.getOrElse("searchstr", throw BadRequestException(s"required param searchstr is missing")) @@ -1269,7 +1272,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1280,7 +1282,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) entity(as[CreateResourceApiRequestV1]) { apiRequest => requestContext => val requestMessageFuture = for { userProfile <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) request <- makeCreateResourceRequestMessage( apiRequest = apiRequest, @@ -1291,7 +1294,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1303,7 +1305,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestType = reqtypeParam.getOrElse("") resinfo = resinfoParam.getOrElse(false) @@ -1317,7 +1320,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1326,14 +1328,14 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) parameters("deleteComment".?) { deleteCommentParam => requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield makeResourceDeleteMessage(resIri = resIri, deleteComment = deleteCommentParam, userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1348,7 +1350,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) case "properties" => for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) resIri = stringFormatter.validateAndEscapeIri( iri, @@ -1365,7 +1368,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) requestMessageF = requestMessage, viewHandler = ResourceHtmlView.propertiesHtmlView, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1374,7 +1376,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) get { requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) resIri = stringFormatter.validateAndEscapeIri( iri, @@ -1385,7 +1388,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1396,7 +1398,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) entity(as[ChangeResourceLabelApiRequestV1]) { apiRequest => requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) resIri = stringFormatter.validateAndEscapeIri( iri, @@ -1416,7 +1419,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1427,7 +1429,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) parameters("depth".as[Int].?) { depth => requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) resourceIri = stringFormatter.validateAndEscapeIri( iri, @@ -1438,7 +1441,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1458,7 +1460,6 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRoute( requestMessage = msg, requestContext = requestContext, - settings = settings, appActor = appActor, log = log ) @@ -1468,7 +1469,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) entity(as[String]) { xml => requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) _ = if (userADM.isAnonymousUser) { @@ -1521,10 +1523,9 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV1.runJsonRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, appActor = appActor, log = log - )(timeout = settings.defaultTimeout, executionContext = executionContext) + )(timeout = appConfig.defaultTimeoutAsDuration, executionContext = executionContext) } } } ~ path("v1" / "resources" / "xmlimportschemas" / Segment) { internalOntologyIri => @@ -1549,7 +1550,8 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) ) { requestContext => val httpResponseFuture: Future[HttpResponse] = for { userProfile <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) schemaZipFileBytes: Array[Byte] <- generateSchemaZipFile( internalOntologyIri = internalOntologyIri, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala index 93fedabe27..666cea4060 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala @@ -12,6 +12,7 @@ import scala.language.postfixOps import dsp.errors.BadRequestException import org.knora.webapi.IRI +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.v1.responder.searchmessages.ExtendedSearchGetRequestV1 @@ -27,7 +28,9 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides a spray-routing function for API routes that deal with search. */ -class SearchRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class SearchRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * The default number of rows to show in search results. @@ -247,14 +250,13 @@ class SearchRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit // in the original API, there is a slash after "search": "http://www.salsah.org/api/search/?searchtype=extended" get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext) + userADM <- getUserADM(requestContext, appConfig) params: Map[String, Seq[String]] = requestContext.request.uri.query().toMultiMap } yield makeExtendedSearchRequestMessage(userADM, params) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -264,14 +266,13 @@ class SearchRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext) + userADM <- getUserADM(requestContext, appConfig) params: Map[String, String] = requestContext.request.uri.query().toMap } yield makeFulltextSearchRequestMessage(userADM, searchval, params) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala index d23c7a9904..ebcdc501c5 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala @@ -16,6 +16,7 @@ import scala.concurrent.Future import scala.concurrent.duration._ import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.standoffmessages.RepresentationV1JsonProtocol.createMappingApiRequestV1Format import org.knora.webapi.messages.v1.responder.standoffmessages._ import org.knora.webapi.routing.Authenticator @@ -26,7 +27,9 @@ import org.knora.webapi.routing.RouteUtilV1 /** * A route used to convert XML to standoff. */ -class StandoffRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class StandoffRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -67,7 +70,7 @@ class StandoffRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) w val requestMessageFuture: Future[CreateMappingRequestV1] = for { - userProfile <- getUserADM(requestContext) + userProfile <- getUserADM(requestContext, appConfig) allParts: Map[Name, String] <- allPartsFuture @@ -114,7 +117,6 @@ class StandoffRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) w RouteUtilV1.runJsonRouteWithFuture( requestMessageFuture, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala index 6c5774fb34..73e1d98a75 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala @@ -12,6 +12,7 @@ import org.apache.commons.validator.routines.UrlValidator import java.util.UUID import dsp.errors.BadRequestException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.usermessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -21,7 +22,9 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides a spray-routing function for API routes that deal with lists. */ -class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { private val schemes = Array("http", "https") new UrlValidator(schemes) @@ -36,13 +39,12 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /* return all users */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) } yield UsersGetRequestV1(userProfile) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -55,7 +57,7 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /* check if email or iri was supplied */ val requestMessage = if (identifier == "email") { for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) } yield UserProfileByEmailGetRequestV1( email = value, userProfileType = UserProfileTypeV1.RESTRICTED, @@ -63,7 +65,7 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with ) } else { for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) userIri = stringFormatter.validateAndEscapeIri( value, throw BadRequestException(s"Invalid user IRI $value") @@ -78,7 +80,6 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -90,7 +91,7 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /* get user's project memberships */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) checkedUserIri = stringFormatter.validateAndEscapeIri( userIri, throw BadRequestException(s"Invalid user IRI $userIri") @@ -104,7 +105,6 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -115,7 +115,7 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /* get user's project admin memberships */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) checkedUserIri = stringFormatter.validateAndEscapeIri( userIri, throw BadRequestException(s"Invalid user IRI $userIri") @@ -129,7 +129,6 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -140,7 +139,7 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /* get user's group memberships */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) checkedUserIri = stringFormatter.validateAndEscapeIri( userIri, throw BadRequestException(s"Invalid user IRI $userIri") @@ -154,7 +153,6 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala index 5287a3414a..93882bae22 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala @@ -18,6 +18,7 @@ import dsp.errors.BadRequestException import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages.GetFileMetadataRequest @@ -36,7 +37,9 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides an Akka routing function for API routes that deal with values. */ -class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -154,7 +157,6 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit mappingIri = mappingIri, acceptStandoffLinksToClientIDs = false, userProfile = userADM, - settings = settings, appActor = appActor, log = log ) @@ -372,7 +374,6 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit mappingIri = mappingIri, acceptStandoffLinksToClientIDs = false, userProfile = userADM, - settings = settings, appActor = appActor, log = log ) @@ -582,7 +583,7 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ): Future[ChangeFileValueRequestV1] = { val resourceIri = stringFormatter.validateAndEscapeIri(resIriStr, throw BadRequestException(s"Invalid resource IRI: $resIriStr")) - val tempFilePath = stringFormatter.makeSipiTempFilePath(settings, apiRequest.file) + val tempFilePath = stringFormatter.makeSipiTempFilePath(apiRequest.file) for { fileMetadataResponse: GetFileMetadataResponse <- @@ -611,14 +612,14 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit get { requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield makeVersionHistoryRequestMessage(iris = iris, userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -628,7 +629,8 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit entity(as[CreateValueApiRequestV1]) { apiRequest => requestContext => val requestMessageFuture = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) request <- makeCreateValueRequestMessage(apiRequest = apiRequest, userADM = userADM) } yield request @@ -636,7 +638,6 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV1.runJsonRouteWithFuture( requestMessageFuture, requestContext, - settings, appActor, log ) @@ -646,14 +647,14 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit get { requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield makeGetValueRequest(valueIriStr = valueIriStr, userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -663,7 +664,8 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit // we are getting a request to change either the value or the comment, but not both. val requestMessageFuture = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) request <- apiRequest match { case ChangeValueApiRequestV1(_, _, _, _, _, _, _, _, _, _, _, _, _, Some(comment)) => @@ -686,7 +688,6 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV1.runJsonRouteWithFuture( requestMessageFuture, requestContext, - settings, appActor, log ) @@ -694,7 +695,8 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } ~ delete { requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) params = requestContext.request.uri.query().toMap deleteComment = params.get("deleteComment") @@ -703,7 +705,6 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -712,14 +713,14 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit delete { requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield makeChangeCommentRequestMessage(valueIriStr = valueIriStr, comment = None, userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -729,14 +730,14 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit get { requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield makeLinkValueGetRequestMessage(iris = iris, userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) @@ -746,7 +747,8 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit entity(as[ChangeFileValueApiRequestV1]) { apiRequest => requestContext => val requestMessage = for { userADM <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) resourceIri = stringFormatter.validateAndEscapeIri( resIriStr, @@ -777,7 +779,6 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV1.runJsonRouteWithFuture( requestMessage, requestContext, - settings, appActor, log ) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala index b7fd0b9c07..e983c6dea6 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala @@ -8,6 +8,7 @@ package org.knora.webapi.routing.v2 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.usersmessages.UserIdentifierADM import org.knora.webapi.messages.v2.routing.authenticationmessages.AuthenticationV2JsonProtocol import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraPasswordCredentialsV2 @@ -19,8 +20,8 @@ import org.knora.webapi.routing.KnoraRouteData /** * A route providing API v2 authentication support. It allows the creation of "sessions", which are used in the SALSAH app. */ -class AuthenticationRouteV2(routeData: KnoraRouteData) - extends KnoraRoute(routeData) +class AuthenticationRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) with Authenticator with AuthenticationV2JsonProtocol { @@ -33,7 +34,8 @@ class AuthenticationRouteV2(routeData: KnoraRouteData) requestContext => requestContext.complete { doAuthenticateV2( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } } ~ @@ -61,7 +63,8 @@ class AuthenticationRouteV2(routeData: KnoraRouteData) maybeUsername = apiRequest.username ), password = apiRequest.password - ) + ), + appConfig ) } } @@ -69,7 +72,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData) delete { // logout requestContext => requestContext.complete { - doLogoutV2(requestContext) + doLogoutV2(requestContext, appConfig) } } } ~ @@ -77,7 +80,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData) get { // html login interface (necessary for IIIF Authentication API support) requestContext => requestContext.complete { - presentLoginFormV2(requestContext) + presentLoginFormV2(requestContext, appConfig) } } ~ post { // called by html login interface (necessary for IIIF Authentication API support) @@ -90,7 +93,8 @@ class AuthenticationRouteV2(routeData: KnoraRouteData) maybeUsername = Some(username) ), password = password - ) + ), + appConfig ) } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala index f495b1ef12..a9f85d719c 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala @@ -12,6 +12,7 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v2.responder.listsmessages.ListGetRequestV2 import org.knora.webapi.messages.v2.responder.listsmessages.NodeGetRequestV2 import org.knora.webapi.routing.Authenticator @@ -22,7 +23,9 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a function for API routes that deal with lists and nodes. */ -class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class ListsRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -37,7 +40,8 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with requestContext => val requestMessage: Future[ListGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) listIri: IRI = stringFormatter.validateAndEscapeIri( lIri, @@ -51,7 +55,7 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -66,7 +70,8 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with requestContext => val requestMessage: Future[NodeGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) nodeIri: IRI = stringFormatter.validateAndEscapeIri( nIri, @@ -80,7 +85,7 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, 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 791c355051..fc9c3fecc9 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 @@ -23,6 +23,7 @@ import dsp.valueobjects.LangString import dsp.valueobjects.Schema._ import org.knora.webapi.ApiV2Complex import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -40,7 +41,9 @@ import org.knora.webapi.routing.RouteUtilV2 /** * 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, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { val ontologiesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "ontologies") @@ -85,7 +88,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) // This is the route used to dereference an actual ontology IRI. If the URL path looks like it // belongs to a built-in API ontology (which has to contain "knora-api"), prefix it with // http://api.knora.org to get the ontology IRI. Otherwise, if it looks like it belongs to a - // project-specific API ontology, prefix it with settings.externalOntologyIriHostAndPort to get the + // project-specific API ontology, prefix it with appConfig.externalOntologyIriHostAndPort to get the // ontology IRI. val urlPath = requestContext.request.uri.path.toString @@ -93,7 +96,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestedOntologyStr: IRI = if (stringFormatter.isBuiltInApiV2OntologyUrlPath(urlPath)) { OntologyConstants.KnoraApi.ApiOntologyHostname + urlPath } else if (stringFormatter.isProjectSpecificApiV2OntologyUrlPath(urlPath)) { - "http://" + settings.externalOntologyIriHostAndPort + urlPath + "http://" + appConfig.knoraApi.externalOntologyIriHostAndPort + urlPath } else { throw BadRequestException(s"Invalid or unknown URL path for external ontology: $urlPath") } @@ -118,7 +121,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[OntologyEntitiesGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield OntologyEntitiesGetRequestV2( ontologyIri = requestedOntology, @@ -129,7 +133,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -145,7 +149,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[OntologyMetadataGetByProjectRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield OntologyMetadataGetByProjectRequestV2( projectIris = maybeProjectIri.toSet, @@ -155,7 +160,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -173,7 +178,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[ChangeOntologyMetadataRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: ChangeOntologyMetadataRequestV2 <- ChangeOntologyMetadataRequestV2.fromJsonLD( @@ -181,7 +187,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -189,7 +194,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -205,7 +210,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) get { requestContext => val requestMessageFuture: Future[OntologyMetadataGetByProjectRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) validatedProjectIris = @@ -220,7 +226,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -252,7 +258,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[OntologyEntitiesGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield OntologyEntitiesGetRequestV2( ontologyIri = requestedOntologyIri, @@ -263,7 +270,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -279,7 +286,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[CreateClassRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -289,7 +297,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -297,7 +304,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -316,7 +323,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[ChangeClassLabelsOrCommentsRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -326,7 +334,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -334,7 +341,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -367,7 +374,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeleteClassCommentRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield DeleteClassCommentRequestV2( classIri = classIri, @@ -379,7 +387,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -396,7 +404,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[AddCardinalitiesToClassRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -406,7 +415,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -414,7 +422,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -437,7 +445,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[CanChangeCardinalitiesRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield CanChangeCardinalitiesRequestV2( classIri = classIri, @@ -447,7 +456,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -465,7 +474,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[ChangeCardinalitiesRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -475,7 +485,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -483,7 +492,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -501,7 +510,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[CanDeleteCardinalitiesFromClassRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -512,7 +522,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -520,7 +529,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -540,7 +549,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[DeleteCardinalitiesFromClassRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -550,7 +560,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -558,7 +567,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -577,7 +586,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[ChangeGuiOrderRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -587,7 +597,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -595,7 +604,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -650,7 +659,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[ClassesGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ClassesGetRequestV2( classIris = classesForResponder, @@ -661,7 +671,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -682,7 +692,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[CanDeleteClassRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield CanDeleteClassRequestV2( classIri = classIri, @@ -692,7 +703,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -727,7 +738,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeleteClassRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield DeleteClassRequestV2( classIri = classIri, @@ -739,7 +751,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -769,7 +781,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeleteOntologyCommentRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield DeleteOntologyCommentRequestV2( ontologyIri = ontologyIri, @@ -781,7 +794,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -798,7 +811,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[CreatePropertyRequestV2] = for { - requestingUser: UserADM <- getUserADM(requestContext = requestContext) + requestingUser: UserADM <- getUserADM(requestContext = requestContext, appConfig) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -851,7 +864,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -992,7 +1004,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1011,7 +1023,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[ChangePropertyLabelsOrCommentsRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -1022,7 +1035,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -1030,7 +1042,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1063,7 +1075,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeletePropertyCommentRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield DeletePropertyCommentRequestV2( propertyIri = propertyIri, @@ -1075,7 +1088,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1092,7 +1105,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[ChangePropertyGuiElementRequest] = for { - requestingUser: UserADM <- getUserADM(requestContext = requestContext) + requestingUser: UserADM <- getUserADM(requestContext = requestContext, appConfig) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -1155,7 +1168,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1210,7 +1223,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[PropertiesGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield PropertiesGetRequestV2( propertyIris = propsForResponder, @@ -1221,7 +1235,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -1242,7 +1256,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[CanDeletePropertyRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield CanDeletePropertyRequestV2( propertyIri = propertyIri, @@ -1252,7 +1267,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1287,7 +1302,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeletePropertyRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield DeletePropertyRequestV2( propertyIri = propertyIri, @@ -1299,7 +1315,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1315,7 +1331,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) { val requestMessageFuture: Future[CreateOntologyRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -1325,7 +1342,6 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -1333,7 +1349,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1356,7 +1372,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[CanDeleteOntologyRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield CanDeleteOntologyRequestV2( ontologyIri = ontologyIri, @@ -1366,7 +1383,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1398,7 +1415,8 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeleteOntologyRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield DeleteOntologyRequestV2( ontologyIri = ontologyIri, @@ -1410,7 +1428,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index e7753dc6d0..70970bb922 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -15,6 +15,7 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter @@ -26,11 +27,20 @@ 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.messages.v2.responder.valuemessages.StillImageFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.DocumentFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.ArchiveFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.TextFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.AudioFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.MovingImageFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2 /** * Provides a routing function for API v2 routes that deal with resources. */ -class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") @@ -74,7 +84,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[ResourceIIIFManifestGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ResourceIIIFManifestGetRequestV2( resourceIri = resourceIri, @@ -84,7 +95,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -101,7 +112,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[CreateResourceRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: CreateResourceRequestV2 <- CreateResourceRequestV2.fromJsonLD( @@ -109,15 +121,19 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) + + // check for each value which represents a file value if the file's MIME type is allowed + _ <- checkMimeTypesForFileValueContents( + values = requestMessage.createResource.flatValues + ) } yield requestMessage RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -136,7 +152,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[UpdateResourceMetadataRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: UpdateResourceMetadataRequestV2 <- UpdateResourceMetadataRequestV2.fromJsonLD( @@ -144,7 +161,6 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -152,7 +168,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -205,7 +221,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[SearchResourcesByProjectAndClassRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield SearchResourcesByProjectAndClassRequestV2( projectIri = projectIri, @@ -220,7 +237,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -252,7 +269,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[ResourceVersionHistoryGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ResourceVersionHistoryGetRequestV2( resourceIri = resourceIri, @@ -264,7 +282,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -278,7 +296,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) get { requestContext => val requestMessageFuture: Future[ResourceHistoryEventsGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ResourceHistoryEventsGetRequestV2( resourceIri = resourceIri, @@ -288,7 +307,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -302,7 +321,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) get { requestContext => val requestMessageFuture: Future[ProjectResourcesWithHistoryGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ProjectResourcesWithHistoryGetRequestV2( projectIri = projectIri, @@ -312,7 +332,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -323,8 +343,10 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) private def getResources(): Route = path(resourcesBasePath / Segments) { resIris: Seq[String] => get { requestContext => - if (resIris.size > settings.v2ResultsPerPage) - throw BadRequestException(s"List of provided resource Iris exceeds limit of ${settings.v2ResultsPerPage}") + if (resIris.size > appConfig.v2.resourcesSequence.resultsPerPage) + throw BadRequestException( + s"List of provided resource Iris exceeds limit of ${appConfig.v2.resourcesSequence.resultsPerPage}" + ) val resourceIris: Seq[IRI] = resIris.map { resIri: String => stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: <$resIri>")) @@ -350,7 +372,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[ResourcesGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ResourcesGetRequestV2( resourceIris = resourceIris, @@ -363,7 +386,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -375,8 +398,10 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) private def getResourcesPreview(): Route = path("v2" / "resourcespreview" / Segments) { resIris: Seq[String] => get { requestContext => - if (resIris.size > settings.v2ResultsPerPage) - throw BadRequestException(s"List of provided resource Iris exceeds limit of ${settings.v2ResultsPerPage}") + if (resIris.size > appConfig.v2.resourcesSequence.resultsPerPage) + throw BadRequestException( + s"List of provided resource Iris exceeds limit of ${appConfig.v2.resourcesSequence.resultsPerPage}" + ) val resourceIris: Seq[IRI] = resIris.map { resIri: String => stringFormatter.validateAndEscapeIri(resIri, throw BadRequestException(s"Invalid resource IRI: <$resIri>")) @@ -386,7 +411,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[ResourcesPreviewGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ResourcesPreviewGetRequestV2( resourceIris = resourceIris, @@ -397,7 +423,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -424,7 +450,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[ResourceTEIGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ResourceTEIGetRequestV2( resourceIri = resourceIri, @@ -453,14 +480,14 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) throw BadRequestException(s"Invalid resource IRI: <$resIriStr>") ) val params: Map[String, String] = requestContext.request.uri.query().toMap - val depth: Int = params.get(Depth).map(_.toInt).getOrElse(settings.defaultGraphDepth) + val depth: Int = params.get(Depth).map(_.toInt).getOrElse(appConfig.v2.graphRoute.defaultGraphDepth) if (depth < 1) { throw BadRequestException(s"$Depth must be at least 1") } - if (depth > settings.maxGraphDepth) { - throw BadRequestException(s"$Depth cannot be greater than ${settings.maxGraphDepth}") + if (depth > appConfig.v2.graphRoute.maxGraphDepth) { + throw BadRequestException(s"$Depth cannot be greater than ${appConfig.v2.graphRoute.maxGraphDepth}") } val direction: String = params.getOrElse(Direction, Outbound) @@ -479,7 +506,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[GraphDataGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield GraphDataGetRequestV2( resourceIri = resourceIri, @@ -493,7 +521,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -510,14 +538,14 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeleteOrEraseResourceRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: DeleteOrEraseResourceRequestV2 <- DeleteOrEraseResourceRequestV2.fromJsonLD( requestDoc, apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -525,7 +553,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -544,7 +572,8 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) val requestMessageFuture: Future[DeleteOrEraseResourceRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: DeleteOrEraseResourceRequestV2 <- DeleteOrEraseResourceRequestV2.fromJsonLD( @@ -552,7 +581,6 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage.copy(erase = true) @@ -560,7 +588,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -656,4 +684,57 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) case None => None } } + + /** + * Checks if the MIME types of the given values are allowed by the configuration + * + * @param values the values to be checked. + */ + private def checkMimeTypesForFileValueContents( + values: Iterable[CreateValueInNewResourceV2] + ): Future[Unit] = + Future { + def badRequestException(fileValueContent: FileValueContentV2): BadRequestException = + BadRequestException( + s"File ${fileValueContent.fileValue.internalFilename} has MIME type ${fileValueContent.fileValue.internalMimeType}, which is not supported for still image files" + ) + values.foreach { value => + value.valueContent match { + case fileValueContent: StillImageFileValueContentV2 => { + println("StillImageFileValueContentV2") + if (!appConfig.sipi.imageMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + throw badRequestException(fileValueContent) + } + } + case fileValueContent: DocumentFileValueContentV2 => { + if (!appConfig.sipi.documentMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + throw badRequestException(fileValueContent) + } + } + case fileValueContent: ArchiveFileValueContentV2 => { + if (!appConfig.sipi.archiveMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + throw badRequestException(fileValueContent) + } + } + case fileValueContent: TextFileValueContentV2 => { + println("TextFileValueContentV2") + if (!appConfig.sipi.textMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + throw badRequestException(fileValueContent) + } + } + case fileValueContent: AudioFileValueContentV2 => { + if (!appConfig.sipi.audioMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + throw badRequestException(fileValueContent) + } + } + case fileValueContent: MovingImageFileValueContentV2 => { + if (!appConfig.sipi.videoMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + throw badRequestException(fileValueContent) + } + } + case _ => () + + } + } + } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala index 2b8b21ec1b..18c48c2aae 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala @@ -12,6 +12,7 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -26,7 +27,9 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a function for API routes that deal with search. */ -class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { private val LIMIT_TO_PROJECT = "limitToProject" private val LIMIT_TO_RESOURCE_CLASS = "limitToResourceClass" @@ -168,9 +171,9 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit throw BadRequestException(s"Invalid search string: '$searchStr'") ) - if (escapedSearchStr.length < settings.searchValueMinLength) { + if (escapedSearchStr.length < appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." + s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." ) } @@ -183,7 +186,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val limitToStandoffClass: Option[SmartIri] = getStandoffClass(params) val requestMessage: Future[FullTextSearchCountRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield FullTextSearchCountRequestV2( searchValue = escapedSearchStr, limitToProject = limitToProject, @@ -195,7 +198,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -219,9 +222,9 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit throw BadRequestException(s"Invalid search string: '$searchStr'") ) - if (escapedSearchStr.length < settings.searchValueMinLength) { + if (escapedSearchStr.length < appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." + s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." ) } @@ -244,7 +247,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val schemaOptions: Set[SchemaOption] = RouteUtilV2.getSchemaOptions(requestContext) val requestMessage: Future[FulltextSearchRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield FulltextSearchRequestV2( searchValue = escapedSearchStr, offset = offset, @@ -260,7 +263,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -276,7 +279,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val constructQuery = GravsearchParser.parseQuery(gravsearchQuery) val requestMessage: Future[GravsearchCountRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GravsearchCountRequestV2( constructQuery = constructQuery, requestingUser = requestingUser @@ -285,7 +288,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -301,7 +304,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit { val constructQuery = GravsearchParser.parseQuery(gravsearchQuery) val requestMessage: Future[GravsearchCountRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GravsearchCountRequestV2( constructQuery = constructQuery, requestingUser = requestingUser @@ -310,7 +313,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -330,7 +333,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val schemaOptions: Set[SchemaOption] = RouteUtilV2.getSchemaOptions(requestContext) val requestMessage: Future[GravsearchRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GravsearchRequestV2( constructQuery = constructQuery, targetSchema = targetSchema, @@ -341,7 +344,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -359,7 +362,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val schemaOptions: Set[SchemaOption] = RouteUtilV2.getSchemaOptions(requestContext) val requestMessage: Future[GravsearchRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield GravsearchRequestV2( constructQuery = constructQuery, targetSchema = targetSchema, @@ -370,7 +373,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -391,9 +394,9 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit throw BadRequestException(s"Invalid search string: '$searchval'") ) - if (searchString.length < settings.searchValueMinLength) { + if (searchString.length < appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." + s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." ) } @@ -404,7 +407,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val limitToResourceClass: Option[SmartIri] = getResourceClassFromParams(params) val requestMessage: Future[SearchResourceByLabelCountRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield SearchResourceByLabelCountRequestV2( searchValue = searchString, limitToProject = limitToProject, @@ -415,7 +418,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -434,9 +437,9 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit throw BadRequestException(s"Invalid search string: '$searchval'") ) - if (searchString.length < settings.searchValueMinLength) { + if (searchString.length < appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${settings.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." + s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." ) } @@ -451,7 +454,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val targetSchema: ApiV2Schema = RouteUtilV2.getOntologySchema(requestContext) val requestMessage: Future[SearchResourceByLabelRequestV2] = for { - requestingUser <- getUserADM(requestContext) + requestingUser <- getUserADM(requestContext, appConfig) } yield SearchResourceByLabelRequestV2( searchValue = searchString, offset = offset, @@ -464,7 +467,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala index 45ab87997d..f8a4a8477f 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala @@ -16,6 +16,7 @@ import scala.concurrent.duration._ import dsp.errors.BadRequestException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.rdf.JsonLDUtil @@ -31,7 +32,9 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a function for API routes that deal with search. */ -class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class StandoffRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { /** * Returns the route. @@ -63,7 +66,8 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w val requestMessageFuture: Future[GetStandoffPageRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield GetStandoffPageRequestV2( resourceIri = resourceIri.toString, @@ -76,7 +80,7 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -121,7 +125,8 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w val requestMessageFuture: Future[CreateMappingRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) allParts: Map[Name, String] <- allPartsFuture jsonldDoc = @@ -139,7 +144,6 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w apiRequestID = apiRequestID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) @@ -160,7 +164,7 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala index 9a1e6f0412..6efa9f981e 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala @@ -15,6 +15,7 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.rdf.JsonLDDocument @@ -29,7 +30,9 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a routing function for API v2 routes that deal with values. */ -class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { +class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) + extends KnoraRoute(routeData, appConfig) + with Authenticator { val valuesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "values") @@ -78,7 +81,8 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessageFuture: Future[ResourcesGetRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) } yield ResourcesGetRequestV2( resourceIris = Seq(resourceIri.toString), @@ -91,7 +95,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -108,14 +112,14 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessageFuture: Future[CreateValueRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: CreateValueRequestV2 <- CreateValueRequestV2.fromJsonLD( requestDoc, apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -123,7 +127,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -142,14 +146,14 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessageFuture: Future[UpdateValueRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: UpdateValueRequestV2 <- UpdateValueRequestV2.fromJsonLD( requestDoc, apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -157,7 +161,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -176,14 +180,14 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessageFuture: Future[DeleteValueRequestV2] = for { requestingUser <- getUserADM( - requestContext = requestContext + requestContext = requestContext, + appConfig ) requestMessage: DeleteValueRequestV2 <- DeleteValueRequestV2.fromJsonLD( requestDoc, apiRequestID = UUID.randomUUID, requestingUser = requestingUser, appActor = appActor, - settings = settings, log = log ) } yield requestMessage @@ -191,7 +195,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - settings = settings, + appConfig = appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala index 9ef62f18b6..d5ceb03d2e 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala @@ -60,9 +60,8 @@ case class IIIFServiceSipiImpl( import SipiKnoraJsonResponseProtocol._ for { - url <- ZIO.succeed(config.sipi.internalBaseUrl + getFileMetadataRequest.filePath + "/knora.json") - request <- ZIO.succeed(new HttpGet(url)) - // _ <- ZIO.debug(request) + url <- ZIO.succeed(config.sipi.internalBaseUrl + getFileMetadataRequest.filePath + "/knora.json") + request <- ZIO.succeed(new HttpGet(url)) sipiResponseStr <- doSipiRequest(request).orDie sipiResponse <- ZIO.attempt(sipiResponseStr.parseJson.convertTo[SipiKnoraJsonResponse]).orDie } yield GetFileMetadataResponse( diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala index 6a26922a45..492df75dba 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceHttpConnectorImpl.scala @@ -532,7 +532,7 @@ case class TriplestoreServiceHttpConnectorImpl( * Initialize the Jena Fuseki triplestore. Currently only works for * 'knora-test' and 'knora-test-unit' repository names. To be used, the * API needs to be started with 'KNORA_WEBAPI_TRIPLESTORE_AUTOINIT' set - * to 'true' (settings.triplestoreAutoInit). This is set to `true` for tests + * to 'true' (appConfig.triplestore.autoInit). This is set to `true` for tests * (`test/resources/test.conf`). Usage is only recommended for automated * testing and not for production use. */ diff --git a/webapi/src/main/scala/org/knora/webapi/util/FileUtil.scala b/webapi/src/main/scala/org/knora/webapi/util/FileUtil.scala index 0e8f37aba0..1e1b542bef 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/FileUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/FileUtil.scala @@ -23,7 +23,7 @@ import scala.util.Try import dsp.errors.FileWriteException import dsp.errors.NotFoundException -import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.config.AppConfig /** * Functions for reading and writing files. @@ -125,31 +125,18 @@ object FileUtil { } /** - * Saves data to a temporary file. + * Creates an empty file in the default temporary-file directory specified in the application's configuration * - * @param settings Knora application settings. - * @param binaryData the binary file data to be saved. - * @return the location where the file has been written to. - */ - def saveFileToTmpLocation(settings: KnoraSettingsImpl, binaryData: Array[Byte]): Path = { - val file = createTempFile(settings) - Files.write(file, binaryData) - file - } - - /** - * Creates an empty file in the default temporary-file directory specified in Knora's application settings. - * - * @param settings Knora's application settings. + * @param appConfig the application's configuration * @param fileExtension the extension to be used for the temporary file name, if any, * @return the location where the file has been written to. */ - def createTempFile(settings: KnoraSettingsImpl, fileExtension: Option[String] = None): Path = { - val tempDataDir = Paths.get(settings.tmpDataDir) + def createTempFile(fileExtension: Option[String] = None, appConfig: AppConfig): Path = { + val tempDataDir = Paths.get(appConfig.tmpDatadir) // check if the location for writing temporary files exists if (!Files.exists(tempDataDir)) { - throw FileWriteException(s"Data directory ${settings.tmpDataDir} does not exist on server") + throw FileWriteException(s"Data directory ${appConfig.tmpDatadir} does not exist on server") } val extension = fileExtension.getOrElse("bin") diff --git a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala index 183165ddf2..f35a4019fb 100644 --- a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala @@ -22,8 +22,6 @@ 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.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.store.cache.settings.CacheServiceSettings import org.knora.webapi.util.LogAspect @@ -84,14 +82,14 @@ abstract class CoreSpec implicit lazy val system: actor.ActorSystem = router.system implicit lazy val executionContext: ExecutionContext = system.dispatcher - implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) - lazy val rdfDataObjects = List.empty[RdfDataObject] - val log: Logger = Logger(this.getClass()) - val appActor = router.ref + // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) + lazy val rdfDataObjects = List.empty[RdfDataObject] + val log: Logger = Logger(this.getClass()) + val appActor = router.ref // needed by some tests val cacheServiceSettings = new CacheServiceSettings(system.settings.config) - val responderData = ResponderData(system, appActor, settings, cacheServiceSettings) + val responderData = ResponderData(system, appActor, cacheServiceSettings) val appConfig = config final override def beforeAll(): Unit = diff --git a/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala index c979df2581..9d75cac6a4 100644 --- a/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala @@ -38,7 +38,6 @@ import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol import org.knora.webapi.messages.util.rdf._ import org.knora.webapi.routing.KnoraRouteData -import org.knora.webapi.settings._ import org.knora.webapi.testservices.FileToUpload import org.knora.webapi.testservices.TestClientService import org.knora.webapi.util.FileUtil @@ -107,14 +106,15 @@ abstract class E2ESpec implicit lazy val system: akka.actor.ActorSystem = router.system implicit lazy val executionContext: ExecutionContext = system.dispatcher - implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) - lazy val rdfDataObjects = List.empty[RdfDataObject] - val log: Logger = Logger(this.getClass()) - val appActor = router.ref + // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) + lazy val rdfDataObjects = List.empty[RdfDataObject] + val log: Logger = Logger(this.getClass()) + val appActor = router.ref // needed by some tests - val routeData = KnoraRouteData(system, appActor) - val baseApiUrl = settings.internalKnoraApiBaseUrl + val appConfig = config + val routeData = KnoraRouteData(system, appActor, appConfig) + val baseApiUrl = appConfig.knoraApi.internalKnoraApiBaseUrl final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ @@ -256,23 +256,23 @@ abstract class E2ESpec Files.createDirectories(newOutputFile.getParent) FileUtil.writeTextFile( newOutputFile, - responseAsString.replaceAll(settings.externalSipiIIIFGetUrl, "IIIF_BASE_URL") + responseAsString.replaceAll(appConfig.sipi.externalBaseUrl, "IIIF_BASE_URL") ) responseAsString } else { - FileUtil.readTextFile(file).replaceAll("IIIF_BASE_URL", settings.externalSipiIIIFGetUrl) + FileUtil.readTextFile(file).replaceAll("IIIF_BASE_URL", appConfig.sipi.externalBaseUrl) } private def createTmpFileDir(): Unit = { // check if tmp datadir exists and create it if not - val tmpFileDir = Path.of(settings.tmpDataDir) + val tmpFileDir = Path.of(appConfig.tmpDatadir) if (!Files.exists(tmpFileDir)) { try { Files.createDirectories(tmpFileDir) } catch { case e: Throwable => - throw FileWriteException(s"Tmp data directory ${settings.tmpDataDir} could not be created: ${e.getMessage}") + throw FileWriteException(s"Tmp data directory ${appConfig.tmpDatadir} could not be created: ${e.getMessage}") } } } diff --git a/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala index 08e2464f6a..99b0f72991 100644 --- a/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala @@ -31,7 +31,6 @@ import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.messages.util.rdf.JsonLDUtil -import org.knora.webapi.settings._ import org.knora.webapi.testservices.FileToUpload import org.knora.webapi.testservices.TestClientService import org.knora.webapi.util.LogAspect @@ -99,10 +98,11 @@ abstract class ITKnoraLiveSpec implicit lazy val system: akka.actor.ActorSystem = router.system implicit lazy val executionContext: ExecutionContext = system.dispatcher - implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) - lazy val rdfDataObjects = List.empty[RdfDataObject] - val log: Logger = Logger(this.getClass()) - val appActor = router.ref + // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) + lazy val rdfDataObjects = List.empty[RdfDataObject] + val log: Logger = Logger(this.getClass()) + val appActor = router.ref + val appConfig = config // needed by some tests val baseApiUrl = config.knoraApi.internalKnoraApiBaseUrl diff --git a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala index 46b07ebdc8..a85984515b 100644 --- a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala @@ -27,8 +27,6 @@ import org.knora.webapi.core.TestStartupUtils import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.util.rdf._ import org.knora.webapi.routing.KnoraRouteData -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.util.FileUtil import org.knora.webapi.util.LogAspect @@ -90,13 +88,14 @@ abstract class R2RSpec } // main difference to other specs (no own systen and executionContext defined) - implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) - lazy val rdfDataObjects = List.empty[RdfDataObject] - val log: Logger = Logger(this.getClass()) - val appActor = router.ref + // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) + lazy val rdfDataObjects = List.empty[RdfDataObject] + val log: Logger = Logger(this.getClass()) + val appActor = router.ref // needed by some tests - val routeData = KnoraRouteData(system, appActor) + val routeData = KnoraRouteData(system, appActor, config) + val appConfig = config final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ @@ -154,10 +153,10 @@ abstract class R2RSpec Files.createDirectories(newOutputFile.getParent) FileUtil.writeTextFile( newOutputFile, - responseAsString.replaceAll(settings.externalSipiIIIFGetUrl, "IIIF_BASE_URL") + responseAsString.replaceAll(appConfig.sipi.externalBaseUrl, "IIIF_BASE_URL") ) responseAsString } else { - FileUtil.readTextFile(file).replaceAll("IIIF_BASE_URL", settings.externalSipiIIIFGetUrl) + FileUtil.readTextFile(file).replaceAll("IIIF_BASE_URL", appConfig.sipi.externalBaseUrl) } } diff --git a/webapi/src/test/scala/org/knora/webapi/core/TestClientService.scala b/webapi/src/test/scala/org/knora/webapi/core/TestClientService.scala index ea52d80b95..b623973718 100644 --- a/webapi/src/test/scala/org/knora/webapi/core/TestClientService.scala +++ b/webapi/src/test/scala/org/knora/webapi/core/TestClientService.scala @@ -1,7 +1,6 @@ package org.knora.webapi.testservices import akka.http.scaladsl.client.RequestBuilding -import akka.stream.Materializer import org.apache.http import org.apache.http.HttpHost import org.apache.http.client.config.RequestConfig @@ -37,8 +36,6 @@ import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtoc import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.messages.util.rdf.JsonLDUtil import org.knora.webapi.settings.KnoraDispatchers -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util.SipiUtil @@ -64,8 +61,6 @@ final case class TestClientService(config: AppConfig, httpClient: CloseableHttpC with RequestBuilding { implicit val system: akka.actor.ActorSystem = sys - implicit val settings: KnoraSettingsImpl = KnoraSettings(system) - implicit val materializer: Materializer = Materializer.matFromSystem(system) implicit val executionContext: ExecutionContext = system.dispatchers.lookup(KnoraDispatchers.KnoraBlockingDispatcher) case class TestClientTimeoutException(msg: String) extends Exception diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala index d291a7112c..bb5ab02783 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala @@ -21,7 +21,7 @@ import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject */ class CORSSupportE2ESpec extends E2ESpec { - implicit def default(implicit system: ActorSystem) = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem) = RouteTestTimeout(appConfig.defaultTimeoutAsDuration) override lazy val rdfDataObjects = List( RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything") diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/ClientTestDataCollector.scala b/webapi/src/test/scala/org/knora/webapi/e2e/ClientTestDataCollector.scala index 464e35b4cc..7c78a71c4a 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/ClientTestDataCollector.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/ClientTestDataCollector.scala @@ -11,20 +11,20 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path -import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.config.AppConfig /** * Collects E2E test requests and responses for use as client test data. * * @param settings the application settings. */ -class ClientTestDataCollector(settings: KnoraSettingsImpl) extends LazyLogging { +class ClientTestDataCollector(appConfig: AppConfig) extends LazyLogging { // write the client test data into this folder private val tempClientTestDataFolder: Path = Path.of("/tmp/client_test_data", "test-data") // Are we configured to collect client test data? - private val collectClientTestData = settings.collectClientTestData + private val collectClientTestData = appConfig.clientTestDataService.collectClientTestData logger.debug(s"collectClientTestData: $collectClientTestData") /** diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/ExceptionHandlerR2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/ExceptionHandlerR2RSpec.scala index f5c3fc74a2..cd91cf12a9 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/ExceptionHandlerR2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/ExceptionHandlerR2RSpec.scala @@ -74,7 +74,7 @@ class ExceptionHandlerR2RSpec extends R2RSpec { } } - private val route: Route = Route.seal(handleExceptions(handler.KnoraExceptionHandler(settings)) { + private val route: Route = Route.seal(handleExceptions(handler.KnoraExceptionHandler(appConfig)) { nfe ~ fe ~ bce ~ dve ~ oce ~ ece ~ sse ~ unpe ~ ae }) diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/HealthRouteE2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/HealthRouteE2ESpec.scala index ae2d7b818c..38aeeab97a 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/HealthRouteE2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/HealthRouteE2ESpec.scala @@ -20,13 +20,15 @@ import org.knora.webapi.core.domain.AppState */ class HealthRouteE2ESpec extends E2ESpec { - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) // Directory path for generated client test data private val clientTestDataPath: Seq[String] = Seq("system", "health") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) "The Health Route" should { diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/InstanceCheckerSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/InstanceCheckerSpec.scala index 8797cdc4fd..0acc69dec7 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/InstanceCheckerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/InstanceCheckerSpec.scala @@ -22,7 +22,9 @@ import org.knora.webapi.util.FileUtil class InstanceCheckerSpec extends E2ESpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) private val jsonLDInstanceChecker: InstanceChecker = InstanceChecker.getJsonLDChecker diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/FilesADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/FilesADME2ESpec.scala index c6e2ed6ce9..e008583253 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/FilesADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/FilesADME2ESpec.scala @@ -35,7 +35,7 @@ class FilesADME2ESpec extends E2ESpec with SessionJsonProtocol with TriplestoreJ private val normalUserEmailEnc = java.net.URLEncoder.encode(normalUserEmail, "utf-8") private val testPass = java.net.URLEncoder.encode("test", "utf-8") - val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(settings) + val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(appConfig) override lazy val rdfDataObjects = List( RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything") diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/GroupsADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/GroupsADME2ESpec.scala index a23f34d283..e4c31e75d4 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/GroupsADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/GroupsADME2ESpec.scala @@ -36,7 +36,7 @@ class GroupsADME2ESpec extends E2ESpec with GroupsADMJsonProtocol with SessionJs private val clientTestDataPath: Seq[String] = Seq("admin", "groups") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) private val imagesUser01Email = SharedTestDataADM.imagesUser01.email private val testPass = SharedTestDataADM.testPass diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/PermissionsADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/PermissionsADME2ESpec.scala index 23d430c121..f0bd57894c 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/PermissionsADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/PermissionsADME2ESpec.scala @@ -31,7 +31,7 @@ class PermissionsADME2ESpec extends E2ESpec with TriplestoreJsonProtocol { private val clientTestDataPath: Seq[String] = Seq("admin", "permissions") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) private val customDOAPIri = "http://rdfh.ch/permissions/00FF/zTOK3HlWTLGgTO8ZWVnotg" "The Permissions Route ('admin/permissions')" when { "getting permissions" should { diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2ESpec.scala index 2bb04b8577..3722cd4677 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2ESpec.scala @@ -55,7 +55,7 @@ class ProjectsADME2ESpec private val clientTestDataPath: Seq[String] = Seq("admin", "projects") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) override lazy val rdfDataObjects: List[RdfDataObject] = List( RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything") diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala index debe6fb589..f26d6a2daa 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala @@ -85,7 +85,7 @@ class UsersADME2ESpec private val clientTestDataPath: Seq[String] = Seq("admin", "users") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) /** * Convenience method returning the users project memberships. diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/CreateListItemsRouteADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/CreateListItemsRouteADME2ESpec.scala index ba70ea1ee0..824767ca72 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/CreateListItemsRouteADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/CreateListItemsRouteADME2ESpec.scala @@ -43,7 +43,7 @@ class CreateListItemsRouteADME2ESpec private val clientTestDataPath: Seq[String] = Seq("admin", "lists") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) override lazy val rdfDataObjects = List( RdfDataObject(path = "test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"), diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala index 87c28c8b59..1bbb6c97cc 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala @@ -41,7 +41,7 @@ class DeleteListItemsRouteADME2ESpec private val clientTestDataPath: Seq[String] = Seq("admin", "lists") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) // Collects client test data private def collectClientTestData(fileName: String, fileContent: String): Unit = diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala index 884c4764b0..8dd8843e4f 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala @@ -40,7 +40,7 @@ class GetListItemsRouteADME2ESpec private val clientTestDataPath: Seq[String] = Seq("admin", "lists") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) override lazy val rdfDataObjects = List( RdfDataObject(path = "test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"), diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala index 66fdb9baf1..dc4d8f7f22 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala @@ -43,7 +43,7 @@ class UpdateListItemsRouteADME2ESpec private val clientTestDataPath: Seq[String] = Seq("admin", "lists") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) override lazy val rdfDataObjects = List( RdfDataObject(path = "test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"), diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/http/ServerVersionE2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/http/ServerVersionE2ESpec.scala index 0b328b51b1..e54014b379 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/http/ServerVersionE2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/http/ServerVersionE2ESpec.scala @@ -16,7 +16,9 @@ import org.knora.webapi.http.version.ServerVersion * End-to-End (E2E) test specification for testing the server response. */ class ServerVersionE2ESpec extends E2ESpec { - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) "The Server" should { "return the custom 'Server' header with every response" in { diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/AuthenticationV1E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/AuthenticationV1E2ESpec.scala index 3b51daac5c..b773d41be7 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/AuthenticationV1E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/AuthenticationV1E2ESpec.scala @@ -38,7 +38,7 @@ class AuthenticationV1E2ESpec extends E2ESpec with SessionJsonProtocol with Trip private val testPass = java.net.URLEncoder.encode("test", "utf-8") private val wrongPass = java.net.URLEncoder.encode("wrong", "utf-8") - val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(settings) + val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(appConfig) "The Authentication Route ('v1/authenticate') with credentials supplied via URL parameters" should { @@ -100,7 +100,7 @@ class AuthenticationV1E2ESpec extends E2ESpec with SessionJsonProtocol with Trip HttpCookie( KnoraAuthenticationCookieName, value = sid, - domain = Some(settings.cookieDomain), + domain = Some(appConfig.cookieDomain), path = Some("/"), httpOnly = true ) @@ -142,7 +142,7 @@ class AuthenticationV1E2ESpec extends E2ESpec with SessionJsonProtocol with Trip HttpCookie( KnoraAuthenticationCookieName, "", - domain = Some(settings.cookieDomain), + domain = Some(appConfig.cookieDomain), path = Some("/"), httpOnly = true, expires = Some(DateTime(1970, 1, 1, 0, 0, 0)), @@ -197,7 +197,7 @@ class AuthenticationV1E2ESpec extends E2ESpec with SessionJsonProtocol with Trip HttpCookie( KnoraAuthenticationCookieName, value = sid, - domain = Some(settings.cookieDomain), + domain = Some(appConfig.cookieDomain), path = Some("/"), httpOnly = true ) diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala index 48f43f501b..868b840785 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala @@ -55,9 +55,12 @@ import org.knora.webapi.util.MutableTestIri */ class ResourcesV1R2RSpec extends R2RSpec { - private val resourcesPathV1 = DSPApiDirectives.handleErrors(system)(new ResourcesRouteV1(routeData).makeRoute) - private val resourcesPathV2 = DSPApiDirectives.handleErrors(system)(new ResourcesRouteV2(routeData).makeRoute) - private val valuesPathV1 = DSPApiDirectives.handleErrors(system)(new ValuesRouteV1(routeData).makeRoute) + private val resourcesPathV1 = + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV1(routeData, appConfig).makeRoute) + private val resourcesPathV2 = + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, appConfig).makeRoute) + private val valuesPathV1 = + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) private val superUser = SharedTestDataADM.superUser private val superUserEmail = superUser.email @@ -82,7 +85,9 @@ class ResourcesV1R2RSpec extends R2RSpec { private val password = SharedTestDataADM.testPass - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout * 2) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration * 2 + ) implicit val ec: ExecutionContextExecutor = system.dispatcher diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala index 9c1d9c95ba..9238007ef2 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala @@ -24,9 +24,11 @@ import org.knora.webapi.routing.v1.SearchRouteV1 */ class SearchV1R2RSpec extends R2RSpec { - private val searchPath = new SearchRouteV1(routeData).makeRoute + private val searchPath = new SearchRouteV1(routeData, appConfig).makeRoute - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) implicit val ec: ExecutionContextExecutor = system.dispatcher diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala index b005fa5bb8..0f2a43fa1c 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala @@ -31,10 +31,12 @@ import org.knora.webapi.sharedtestdata.SharedTestDataV1 */ class SipiV1R2RSpec extends R2RSpec { - private val resourcesPath = new ResourcesRouteV1(routeData).makeRoute - private val valuesPath = new ValuesRouteV1(routeData).makeRoute + private val resourcesPath = new ResourcesRouteV1(routeData, appConfig).makeRoute + private val valuesPath = new ValuesRouteV1(routeData, appConfig).makeRoute - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) private val incunabulaProjectAdminEmail = SharedTestDataV1.incunabulaProjectAdminUser.userData.email.get private val testPass = "test" @@ -90,14 +92,16 @@ class SipiV1R2RSpec extends R2RSpec { def createTmpFileDir(): Unit = { // check if tmp datadir exists and create it if not - val tmpFileDir = Paths.get(settings.tmpDataDir) + val tmpFileDir = Paths.get(appConfig.tmpDatadir) if (!Files.exists(tmpFileDir)) { try { Files.createDirectories(tmpFileDir) } catch { case e: Throwable => - throw FileWriteException(s"Tmp data directory ${settings.tmpDataDir} could not be created: ${e.getMessage}") + throw FileWriteException( + s"Tmp data directory ${appConfig.tmpDatadir} could not be created: ${e.getMessage}" + ) } } } diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala index f577a7a6ce..93328e1e82 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala @@ -42,15 +42,19 @@ import org.knora.webapi.util.MutableTestIri */ class StandoffV1R2RSpec extends R2RSpec { - private val standoffPath = DSPApiDirectives.handleErrors(system)(new StandoffRouteV1(routeData).makeRoute) - private val valuesPath = DSPApiDirectives.handleErrors(system)(new ValuesRouteV1(routeData).makeRoute) + private val standoffPath = + DSPApiDirectives.handleErrors(system, appConfig)(new StandoffRouteV1(routeData, appConfig).makeRoute) + private val valuesPath = + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) private val anythingUser = SharedTestDataV1.anythingUser1 private val anythingUserEmail = anythingUser.userData.email.get private val password = SharedTestDataADM.testPass - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) implicit val ec: ExecutionContextExecutor = system.dispatcher diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala index 26f5c58532..dd6f05ddf4 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala @@ -29,9 +29,12 @@ import org.knora.webapi.util.MutableTestIri */ class ValuesV1R2RSpec extends R2RSpec { - private val valuesPath = DSPApiDirectives.handleErrors(system)(new ValuesRouteV1(routeData).makeRoute) + private val valuesPath = + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) private val integerValueIri = new MutableTestIri private val timeValueIri = new MutableTestIri diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/AuthenticationV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/AuthenticationV2E2ESpec.scala index 93375b418d..2a7d6d5892 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/AuthenticationV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/AuthenticationV2E2ESpec.scala @@ -196,7 +196,7 @@ class AuthenticationV2E2ESpec extends E2ESpec with AuthenticationV2JsonProtocol } "authenticate with token in cookie" in { - val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(settings) + val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(appConfig) val cookieHeader = headers.Cookie(KnoraAuthenticationCookieName, token.get) val request = Get(baseApiUrl + "/v2/authentication") ~> addHeader(cookieHeader) @@ -205,7 +205,7 @@ class AuthenticationV2E2ESpec extends E2ESpec with AuthenticationV2JsonProtocol } "fail authentication with invalid token in cookie" in { - val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(settings) + val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(appConfig) val cookieHeader = headers.Cookie(KnoraAuthenticationCookieName, "not_a_valid_token") val request = Get(baseApiUrl + "/v2/authentication") ~> addHeader(cookieHeader) diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala index f156048417..97613dc4ae 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala @@ -25,9 +25,11 @@ import org.knora.webapi.routing.v2.ResourcesRouteV2 */ class JSONLDHandlingV2R2RSpec extends R2RSpec { - private val resourcesPath = new ResourcesRouteV2(routeData).makeRoute + private val resourcesPath = new ResourcesRouteV2(routeData, appConfig).makeRoute - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) implicit val ec: ExecutionContextExecutor = system.dispatcher diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala index fd7d56ed4d..e1a79b4a45 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala @@ -31,9 +31,11 @@ import org.knora.webapi.util.FileUtil */ class ListsRouteV2R2RSpec extends R2RSpec { - private val listsPath = new ListsRouteV2(routeData).makeRoute + private val listsPath = new ListsRouteV2(routeData, appConfig).makeRoute - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) implicit val ec: ExecutionContextExecutor = system.dispatcher @@ -41,7 +43,7 @@ class ListsRouteV2R2RSpec extends R2RSpec { private val clientTestDataPath: Seq[String] = Seq("v2", "lists") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) override lazy val rdfDataObjects: List[RdfDataObject] = List( RdfDataObject(path = "test_data/all_data/incunabula-data.ttl", name = "http://www.knora.org/data/0803/incunabula"), diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala index 2c6cdbc016..a4c71e5802 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala @@ -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.e2e.v2 import akka.actor.ActorSystem @@ -58,10 +63,14 @@ class OntologyV2R2RSpec extends R2RSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val ontologiesPath = DSPApiDirectives.handleErrors(system)(new OntologiesRouteV2(routeData).makeRoute) - private val resourcesPath = DSPApiDirectives.handleErrors(system)(new ResourcesRouteV2(routeData).makeRoute) + private val ontologiesPath = + DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData, appConfig).makeRoute) + private val resourcesPath = + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, appConfig).makeRoute) - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) implicit val ec: ExecutionContextExecutor = system.dispatcher @@ -93,7 +102,7 @@ class OntologyV2R2RSpec extends R2RSpec { private val clientTestDataPath: Seq[String] = Seq("v2", "ontologies") // an instance of ClientTestDataCollector - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) // Collects client test data // TODO: redefine below method somewhere else if can be reused over other test files diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 5667dc4328..df596b89c9 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -25,8 +25,6 @@ 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 @@ -48,6 +46,7 @@ import org.knora.webapi.routing.v2.OntologiesRouteV2 import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.util._ +import scala.concurrent.Await /** * Tests the API v2 resources route. @@ -55,7 +54,9 @@ import org.knora.webapi.util._ class ResourcesRouteV2E2ESpec extends E2ESpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) private val anythingUserEmail = SharedTestDataADM.anythingUser1.email private val password = SharedTestDataADM.testPass @@ -94,7 +95,7 @@ class ResourcesRouteV2E2ESpec extends E2ESpec { private val clientTestDataPath: Seq[String] = Seq("v2", "resources") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) private def collectClientTestData(fileName: String, fileContent: String, fileExtension: String = "json"): Unit = clientTestDataCollector.addFile( @@ -821,7 +822,8 @@ class ResourcesRouteV2E2ESpec extends E2ESpec { "not accept a graph request with an invalid depth (> max)" in { val request = Get( - s"$baseApiUrl/v2/graph/${URLEncoder.encode("http://rdfh.ch/0001/start", "UTF-8")}?depth=${settings.maxGraphBreadth + 1}" + s"$baseApiUrl/v2/graph/${URLEncoder + .encode("http://rdfh.ch/0001/start", "UTF-8")}?depth=${appConfig.v2.graphRoute.maxGraphBreadth + 1}" ) val response: HttpResponse = singleAwaitingRequest(request) val responseAsString = responseToString(response) @@ -1870,79 +1872,79 @@ class ResourcesRouteV2E2ESpec extends E2ESpec { xmlDiff.hasDifferences should be(false) } - "read the large text without its markup, and get the markup separately as pages of standoff" ignore { // depends on previous test - // Get the resource without markup. - val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(hamletResourceIri.get, "UTF-8")}") - .addHeader(new MarkupHeader(RouteUtilV2.MARKUP_STANDOFF)) ~> addCredentials( - BasicHttpCredentials(anythingUserEmail, password) - ) - val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest) - val resourceGetResponseAsString = responseToString(resourceGetResponse) - - // Check that the response matches the ontology. - instanceChecker.check( - instanceResponse = resourceGetResponseAsString, - expectedClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, - knoraRouteGet = doGetRequest - ) - - // Get the standoff markup separately. - val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) - val textValue: JsonLDObject = - resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasRichtext") - val maybeTextValueAsXml: Option[String] = - textValue.maybeString(OntologyConstants.KnoraApiV2Complex.TextValueAsXml) - assert(maybeTextValueAsXml.isEmpty) - val textValueIri: IRI = - textValue.requireStringWithValidation(JsonLDKeywords.ID, stringFormatter.validateAndEscapeIri) - - val resourceIriEncoded: IRI = URLEncoder.encode(hamletResourceIri.get, "UTF-8") - val textValueIriEncoded: IRI = URLEncoder.encode(textValueIri, "UTF-8") - - val standoffBuffer: ArrayBuffer[JsonLDObject] = ArrayBuffer.empty - var offset: Int = 0 - var hasMoreStandoff: Boolean = true - - while (hasMoreStandoff) { - // Get a page of standoff. - - val standoffGetRequest = Get( - s"$baseApiUrl/v2/standoff/$resourceIriEncoded/$textValueIriEncoded/$offset" - ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) - val standoffGetResponse: HttpResponse = singleAwaitingRequest(standoffGetRequest) - val standoffGetResponseAsJsonLD: JsonLDObject = responseToJsonLDDocument(standoffGetResponse).body - - val standoff: Seq[JsonLDValue] = - standoffGetResponseAsJsonLD.maybeArray(JsonLDKeywords.GRAPH).map(_.value).getOrElse(Seq.empty) - - val standoffAsJsonLDObjects: Seq[JsonLDObject] = standoff.map { - case jsonLDObject: JsonLDObject => jsonLDObject - case other => throw AssertionException(s"Expected JsonLDObject, got $other") - } - - standoffBuffer.appendAll(standoffAsJsonLDObjects) - - standoffGetResponseAsJsonLD.maybeInt(OntologyConstants.KnoraApiV2Complex.NextStandoffStartIndex) match { - case Some(nextOffset) => offset = nextOffset - case None => hasMoreStandoff = false - } - } - - assert(standoffBuffer.length == 6738) - - // Check the standoff tags to make sure they match the ontology. - - for (jsonLDObject <- standoffBuffer) { - val docForValidation = JsonLDDocument(body = jsonLDObject).toCompactString() - - instanceChecker.check( - instanceResponse = docForValidation, - expectedClassIri = - jsonLDObject.requireStringWithValidation(JsonLDKeywords.TYPE, stringFormatter.toSmartIriWithErr), - knoraRouteGet = doGetRequest - ) - } - } + // "read the large text without its markup, and get the markup separately as pages of standoff" ignore { // depends on previous test + // // Get the resource without markup. + // val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(hamletResourceIri.get, "UTF-8")}") + // .addHeader(new MarkupHeader(RouteUtilV2.MARKUP_STANDOFF)) ~> addCredentials( + // BasicHttpCredentials(anythingUserEmail, password) + // ) + // val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest) + // val resourceGetResponseAsString = responseToString(resourceGetResponse) + + // // Check that the response matches the ontology. + // instanceChecker.check( + // instanceResponse = resourceGetResponseAsString, + // expectedClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + // knoraRouteGet = doGetRequest + // ) + + // // Get the standoff markup separately. + // val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) + // val textValue: JsonLDObject = + // resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasRichtext") + // val maybeTextValueAsXml: Option[String] = + // textValue.maybeString(OntologyConstants.KnoraApiV2Complex.TextValueAsXml) + // assert(maybeTextValueAsXml.isEmpty) + // val textValueIri: IRI = + // textValue.requireStringWithValidation(JsonLDKeywords.ID, stringFormatter.validateAndEscapeIri) + + // val resourceIriEncoded: IRI = URLEncoder.encode(hamletResourceIri.get, "UTF-8") + // val textValueIriEncoded: IRI = URLEncoder.encode(textValueIri, "UTF-8") + + // val standoffBuffer: ArrayBuffer[JsonLDObject] = ArrayBuffer.empty + // var offset: Int = 0 + // var hasMoreStandoff: Boolean = true + + // while (hasMoreStandoff) { + // // Get a page of standoff. + + // val standoffGetRequest = Get( + // s"$baseApiUrl/v2/standoff/$resourceIriEncoded/$textValueIriEncoded/$offset" + // ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + // val standoffGetResponse: HttpResponse = singleAwaitingRequest(standoffGetRequest) + // val standoffGetResponseAsJsonLD: JsonLDObject = responseToJsonLDDocument(standoffGetResponse).body + + // val standoff: Seq[JsonLDValue] = + // standoffGetResponseAsJsonLD.maybeArray(JsonLDKeywords.GRAPH).map(_.value).getOrElse(Seq.empty) + + // val standoffAsJsonLDObjects: Seq[JsonLDObject] = standoff.map { + // case jsonLDObject: JsonLDObject => jsonLDObject + // case other => throw AssertionException(s"Expected JsonLDObject, got $other") + // } + + // standoffBuffer.appendAll(standoffAsJsonLDObjects) + + // standoffGetResponseAsJsonLD.maybeInt(OntologyConstants.KnoraApiV2Complex.NextStandoffStartIndex) match { + // case Some(nextOffset) => offset = nextOffset + // case None => hasMoreStandoff = false + // } + // } + + // assert(standoffBuffer.length == 6738) + + // // Check the standoff tags to make sure they match the ontology. + + // for (jsonLDObject <- standoffBuffer) { + // val docForValidation = JsonLDDocument(body = jsonLDObject).toCompactString() + + // instanceChecker.check( + // instanceResponse = docForValidation, + // expectedClassIri = + // jsonLDObject.requireStringWithValidation(JsonLDKeywords.TYPE, stringFormatter.toSmartIriWithErr), + // knoraRouteGet = doGetRequest + // ) + // } + // } "erase a resource" in { val resourceLastModificationDate = Instant.parse("2019-02-13T09:05:10Z") @@ -2106,7 +2108,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)(new OntologiesRouteV2(routeData).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData, appConfig).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/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index 90a736915d..3064d1f837 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -51,12 +51,18 @@ import org.knora.webapi.util.MutableTestIri class SearchRouteV2R2RSpec extends R2RSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val searchPath = DSPApiDirectives.handleErrors(system)(new SearchRouteV2(routeData).makeRoute) - private val resourcePath = DSPApiDirectives.handleErrors(system)(new ResourcesRouteV2(routeData).makeRoute) - private val standoffPath = DSPApiDirectives.handleErrors(system)(new StandoffRouteV2(routeData).makeRoute) - private val valuesPath = DSPApiDirectives.handleErrors(system)(new ValuesRouteV1(routeData).makeRoute) - - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + private val searchPath = + DSPApiDirectives.handleErrors(system, appConfig)(new SearchRouteV2(routeData, appConfig).makeRoute) + private val resourcePath = + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, appConfig).makeRoute) + private val standoffPath = + DSPApiDirectives.handleErrors(system, appConfig)(new StandoffRouteV2(routeData, appConfig).makeRoute) + private val valuesPath = + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) + + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) implicit val ec: ExecutionContextExecutor = system.dispatcher @@ -79,7 +85,7 @@ class SearchRouteV2R2RSpec extends R2RSpec { private val clientTestDataPath: Seq[String] = Seq("v2", "search") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) override lazy val rdfDataObjects: List[RdfDataObject] = List( RdfDataObject(path = "test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"), diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala index 831256965f..f42ccd18f4 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala @@ -74,7 +74,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { private val clientTestDataPath: Seq[String] = Seq("v2", "values") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) object AThing { val iri: IRI = "http://rdfh.ch/0001/a-thing" diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala index a715194f0b..110b705e78 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala @@ -37,10 +37,12 @@ class ValuesV2R2RSpec extends R2RSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val valuesPath = new ValuesRouteV2(routeData).makeRoute - private val searchPath = new SearchRouteV2(routeData).makeRoute + private val valuesPath = new ValuesRouteV2(routeData, appConfig).makeRoute + private val searchPath = new SearchRouteV2(routeData, appConfig).makeRoute - implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( + appConfig.defaultTimeoutAsDuration + ) implicit val ec: ExecutionContextExecutor = system.dispatcher @@ -59,7 +61,7 @@ class ValuesV2R2RSpec extends R2RSpec { private val clientTestDataPath: Seq[String] = Seq("v2", "values") // Collects client test data - private val clientTestDataCollector = new ClientTestDataCollector(settings) + private val clientTestDataCollector = new ClientTestDataCollector(appConfig) override lazy val rdfDataObjects = List( RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything") diff --git a/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala b/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala index fb5e17e39b..df3959bad5 100644 --- a/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/it/v1/KnoraSipiIntegrationV1ITSpec.scala @@ -332,7 +332,7 @@ class KnoraSipiIntegrationV1ITSpec "create an 'p0803-incunabula:book' and an 'p0803-incunabula:page' with file parameters via XML import" in { // To be able to run packaged tests inside Docker, we need to copy // the file first to a place which is shared with sipi - val dest: Path = FileUtil.createTempFile(settings, Some("jpg")) + val dest: Path = FileUtil.createTempFile(Some("jpg"), appConfig) Files.copy(pathToChlaus, dest, StandardCopyOption.REPLACE_EXISTING) // Upload the image to Sipi. diff --git a/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiAuthenticationITSpec.scala b/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiAuthenticationITSpec.scala index 59eca12fbb..30c1916887 100644 --- a/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiAuthenticationITSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/it/v2/KnoraSipiAuthenticationITSpec.scala @@ -66,7 +66,7 @@ class KnoraSipiAuthenticationITSpec "successfuly get an image with provided credentials inside cookie" in { // using cookie to authenticate when accessing sipi (test for cookie parsing in sipi) - val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(settings) + val KnoraAuthenticationCookieName = Authenticator.calculateCookieName(appConfig) val cookieHeader = headers.Cookie(KnoraAuthenticationCookieName, loginToken) // Request the permanently stored image from Sipi. diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/KnoraResponseV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/KnoraResponseV2Spec.scala index daad5bdc51..cf2f968315 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/KnoraResponseV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/rdf/KnoraResponseV2Spec.scala @@ -8,10 +8,10 @@ package org.knora.webapi.util.rdf import java.nio.file.Paths import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.util.rdf._ import org.knora.webapi.messages.v2.responder.KnoraJsonLDResponseV2 import org.knora.webapi.messages.v2.responder.KnoraTurtleResponseV2 -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.util.FileUtil /** @@ -117,9 +117,10 @@ class KnoraResponseV2Spec() extends CoreSpec { * A test implementation of [[KnoraJsonLDResponseV2]]. */ case class JsonLDTestResponse(jsonLDDocument: JsonLDDocument) extends KnoraJsonLDResponseV2 { + override protected def toJsonLDDocument( targetSchema: ApiV2Schema, - settings: KnoraSettingsImpl, + appConfig: AppConfig, schemaOptions: Set[SchemaOption] ): JsonLDDocument = jsonLDDocument } @@ -138,7 +139,7 @@ class KnoraResponseV2Spec() extends CoreSpec { rdfFormat = JsonLD, targetSchema = InternalSchema, schemaOptions = Set.empty, - settings = settings + appConfig = appConfig ) // Parse the JSON-LD to a JsonLDDocument. @@ -166,7 +167,7 @@ class KnoraResponseV2Spec() extends CoreSpec { rdfFormat = Turtle, targetSchema = ApiV2Complex, schemaOptions = Set.empty, - settings = settings + appConfig = appConfig ) // Parse the Turtle to an RDF4J Model. @@ -188,7 +189,7 @@ class KnoraResponseV2Spec() extends CoreSpec { rdfFormat = JsonLD, targetSchema = ApiV2Complex, schemaOptions = Set(FlatJsonLD), - settings = settings + appConfig = appConfig ) val jsonLDResponseDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonLDResponseStr) @@ -202,7 +203,7 @@ class KnoraResponseV2Spec() extends CoreSpec { rdfFormat = JsonLD, targetSchema = InternalSchema, schemaOptions = Set(HierarchicalJsonLD), - settings = settings + appConfig = appConfig ) val jsonLDResponseDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonLDResponseStr) @@ -216,7 +217,7 @@ class KnoraResponseV2Spec() extends CoreSpec { rdfFormat = JsonLD, targetSchema = InternalSchema, schemaOptions = Set(FlatJsonLD), - settings = settings + appConfig = appConfig ) val jsonLDResponseDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonLDResponseStr) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala index ec383c8c81..e3f2be30f3 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala @@ -9,6 +9,7 @@ import scala.concurrent.duration._ import dsp.errors.AssertionException import org.knora.webapi.CoreSpec +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -19,7 +20,6 @@ import org.knora.webapi.messages.util.search.gravsearch.GravsearchQueryChecker import org.knora.webapi.messages.util.search.gravsearch.prequery.NonTriplestoreSpecificGravsearchToPrequeryTransformer import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInspectionRunner import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInspectionUtil -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.util.ApacheLuceneSupport.LuceneQueryString @@ -33,7 +33,7 @@ private object QueryHandler { query: String, appActor: ActorRef, responderData: ResponderData, - settings: KnoraSettingsImpl + appConfig: AppConfig )(implicit executionContext: ExecutionContext): SelectQuery = { val constructQuery = GravsearchParser.parseQuery(query) @@ -61,7 +61,7 @@ private object QueryHandler { constructClause = constructQuery.constructClause, typeInspectionResult = typeInspectionResult, querySchema = constructQuery.querySchema.getOrElse(throw AssertionException(s"WhereClause has no querySchema")), - settings = settings + appConfig = appConfig ) val nonTriplestoreSpecificPrequery: SelectQuery = QueryTraverser.transformConstructToSelect( @@ -3206,7 +3206,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryWithOptional, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithOptional) } @@ -3218,7 +3218,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateNonOptionalSortCriterion, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterion) @@ -3232,7 +3232,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateNonOptionalSortCriterionComplex, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterion) @@ -3246,7 +3246,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateNonOptionalSortCriterionAndFilter, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterionAndFilter) @@ -3260,7 +3260,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateNonOptionalSortCriterionAndFilterComplex, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateNonOptionalSortCriterionAndFilter) @@ -3274,7 +3274,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateOptionalSortCriterion, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateOptionalSortCriterion) @@ -3288,7 +3288,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateOptionalSortCriterionComplex, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateOptionalSortCriterion) @@ -3302,7 +3302,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateOptionalSortCriterionAndFilter, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateOptionalSortCriterionAndFilter) @@ -3316,7 +3316,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDateOptionalSortCriterionAndFilterComplex, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDateOptionalSortCriterionAndFilter) @@ -3330,7 +3330,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDecimalOptionalSortCriterion, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterion) @@ -3343,7 +3343,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDecimalOptionalSortCriterionComplex, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterion) @@ -3356,7 +3356,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDecimalOptionalSortCriterionAndFilter, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilter) @@ -3369,7 +3369,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec inputQueryWithDecimalOptionalSortCriterionAndFilterComplex, appActor, responderData, - settings + appConfig ) // TODO: user provided statements and statement generated for sorting should be unified (https://github.com/dhlab-basel/Knora/issues/1195) @@ -3382,7 +3382,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec InputQueryWithRdfsLabelAndLiteralInSimpleSchema, appActor, responderData, - settings + appConfig ) assert(transformedQuery == TransformedQueryWithRdfsLabelAndLiteral) @@ -3394,7 +3394,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec InputQueryWithRdfsLabelAndLiteralInComplexSchema, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithRdfsLabelAndLiteral) @@ -3406,7 +3406,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec InputQueryWithRdfsLabelAndVariableInSimpleSchema, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithRdfsLabelAndVariable) @@ -3418,7 +3418,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec InputQueryWithRdfsLabelAndVariableInComplexSchema, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithRdfsLabelAndVariable) @@ -3430,7 +3430,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec InputQueryWithRdfsLabelAndRegexInSimpleSchema, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithRdfsLabelAndRegex) @@ -3442,7 +3442,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec InputQueryWithRdfsLabelAndRegexInComplexSchema, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithRdfsLabelAndRegex) @@ -3454,7 +3454,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec InputQueryWithUnionScopes, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithUnionScopes) @@ -3466,7 +3466,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryWithStandoffTagHasStartAncestor, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryWithStandoffTagHasStartAncestor) @@ -3478,7 +3478,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryToReorder, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryToReorder) @@ -3490,7 +3490,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryToReorderWithUnion, appActor, responderData, - settings + appConfig ) assert(transformedQuery === transformedQueryToReorderWithUnion) @@ -3502,7 +3502,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryWithOptional, appActor, responderData, - settings + appConfig ) assert(transformedQuery === TransformedQueryWithOptional) @@ -3514,7 +3514,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryToReorderWithMinus, appActor, responderData, - settings + appConfig ) assert(transformedQuery == transformedQueryToReorderWithMinus) @@ -3526,7 +3526,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryToReorderWithCycle, appActor, responderData, - settings + appConfig ) assert(transformedQuery == transformedQueryToReorderWithCycle) @@ -3538,7 +3538,7 @@ class NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec extends CoreSpec queryWithKnoraApiResource, appActor, responderData, - settings + appConfig ) assert( diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala index 606dc60ef6..2b48b555d3 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala @@ -360,7 +360,8 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica // need to be able to authenticate credentials with new password val resF = Authenticator.authenticateCredentialsV2( credentials = - Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test123456")) + Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test123456")), + appConfig )(system, appActor, executionContext) resF map { res => @@ -387,7 +388,8 @@ class UsersResponderADMSpec extends CoreSpec with ImplicitSender with Authentica // need to be able to authenticate credentials with new password val resF = Authenticator.authenticateCredentialsV2( credentials = - Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test654321")) + Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test654321")), + appConfig )(system, appActor, executionContext) resF map { res => diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/SearchResponderV1Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/SearchResponderV1Spec.scala index 273f956d03..ec8541216a 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/SearchResponderV1Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/SearchResponderV1Spec.scala @@ -221,8 +221,8 @@ class SearchResponderV1Spec extends CoreSpec() with ImplicitSender { ), iconlabel = Some("Buch"), icontitle = Some("Buch"), - iconsrc = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), - preview_path = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), + iconsrc = Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), + preview_path = Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), obj_id = "http://rdfh.ch/0803/c5058f3a" ), SearchResultRowV1( @@ -243,8 +243,8 @@ class SearchResponderV1Spec extends CoreSpec() with ImplicitSender { ), iconlabel = Some("Buch"), icontitle = Some("Buch"), - iconsrc = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), - preview_path = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), + iconsrc = Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), + preview_path = Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), obj_id = "http://rdfh.ch/0803/ff17e5ef9601" ) ) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourceUtilV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourceUtilV2Spec.scala index 50e01dab56..7d773706f0 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourceUtilV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourceUtilV2Spec.scala @@ -15,7 +15,7 @@ import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject object ResourceUtilV2Spec {} class ResourceUtilV2Spec extends CoreSpec() { - implicit val timeout: Timeout = settings.defaultTimeout + implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration implicit val ec: ExecutionContextExecutor = system.dispatcher override lazy val rdfDataObjects = List( diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CacheSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CacheSpec.scala index 8c1eb44507..206c6be90b 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CacheSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CacheSpec.scala @@ -33,7 +33,7 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV class CacheSpec extends CoreSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private implicit val timeout: Timeout = settings.defaultTimeout + private implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration override lazy val rdfDataObjects = List( RdfDataObject( diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala index a02129ff0e..60fd5b5069 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ontology/CardinalitiesSpec.scala @@ -19,7 +19,7 @@ import org.knora.webapi.messages.StringFormatter class CardinalitiesSpec extends CoreSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private implicit val timeout: Timeout = settings.defaultTimeout + private implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration val freetestOntologyIri: SmartIri = "http://0.0.0.0:3333/ontology/0001/freetest/v2".toSmartIri diff --git a/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala index 5a9e24452b..d57293a29d 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala @@ -31,7 +31,7 @@ object AuthenticatorSpec { class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodTester { - implicit val timeout: Timeout = settings.defaultTimeout + implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -88,6 +88,7 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT ) val resF = Authenticator invokePrivate authenticateCredentialsV2( Some(correctPasswordCreds), + appConfig, system, appActor, executionContext @@ -101,6 +102,7 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some("wrongemail@example.com")), "wrongpassword") val resF = Authenticator invokePrivate authenticateCredentialsV2( Some(wrongPasswordCreds), + appConfig, system, appActor, executionContext @@ -117,6 +119,7 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT ) val resF = Authenticator invokePrivate authenticateCredentialsV2( Some(wrongPasswordCreds), + appConfig, system, appActor, executionContext @@ -127,14 +130,15 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT } "succeed with correct token" in { val token = JWTHelper.createToken( - "myuseriri", - settings.jwtSecretKey, - settings.jwtLongevity, - settings.externalKnoraApiHostPort + "http://rdfh.ch/users/X-T8IkfQTKa86UWuESpbOA", + appConfig.jwtSecretKey, + appConfig.jwtLongevityAsDuration, + appConfig.knoraApi.externalKnoraApiHostPort ) val tokenCreds = KnoraJWTTokenCredentialsV2(token) val resF = Authenticator invokePrivate authenticateCredentialsV2( Some(tokenCreds), + appConfig, system, appActor, executionContext @@ -145,19 +149,21 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT } "fail with invalidated token" in { val token = JWTHelper.createToken( - "myuseriri", - settings.jwtSecretKey, - settings.jwtLongevity, - settings.externalKnoraApiHostPort + "http://rdfh.ch/users/X-T8IkfQTKa86UWuESpbOA", + appConfig.jwtSecretKey, + appConfig.jwtLongevityAsDuration, + appConfig.knoraApi.externalKnoraApiHostPort ) val tokenCreds = KnoraJWTTokenCredentialsV2(token) CacheUtil.put(AUTHENTICATION_INVALIDATION_CACHE_NAME, tokenCreds.jwtToken, tokenCreds.jwtToken) val resF = Authenticator invokePrivate authenticateCredentialsV2( Some(tokenCreds), + appConfig, system, appActor, executionContext ) + resF map { _ => assertThrows(BadCredentialsException) } @@ -166,6 +172,7 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT val tokenCreds = KnoraJWTTokenCredentialsV2("123456") val resF = Authenticator invokePrivate authenticateCredentialsV2( Some(tokenCreds), + appConfig, system, appActor, executionContext @@ -179,7 +186,7 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT "called, the 'calculateCookieName' method" should { "succeed with generating the name" in { - Authenticator.calculateCookieName(settings) should equal("KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999") + Authenticator.calculateCookieName(appConfig) should equal("KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999") } } } diff --git a/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala index 9d8930c856..16068ed065 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala @@ -21,48 +21,48 @@ class JWTHelperSpec extends CoreSpec with ImplicitSender { "create a token" in { val token: String = JWTHelper.createToken( userIri = SharedTestDataADM.anythingUser1.id, - secret = settings.jwtSecretKey, - longevity = settings.jwtLongevity, - issuer = settings.externalKnoraApiHostPort, + secret = appConfig.jwtSecretKey, + longevity = appConfig.jwtLongevityAsDuration, + issuer = appConfig.knoraApi.externalKnoraApiHostPort, content = Map("foo" -> JsString("bar")) ) JWTHelper.extractUserIriFromToken( token = token, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(Some(SharedTestDataADM.anythingUser1.id)) JWTHelper.extractContentFromToken( token = token, - secret = settings.jwtSecretKey, + secret = appConfig.jwtSecretKey, contentName = "foo", - issuer = settings.externalKnoraApiHostPort + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(Some("bar")) } "validate a token" in { JWTHelper.validateToken( token = validToken, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(true) } "extract the user's IRI" in { JWTHelper.extractUserIriFromToken( token = validToken, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(Some(SharedTestDataADM.anythingUser1.id)) } "extract application-specific content" in { JWTHelper.extractContentFromToken( token = validToken, - secret = settings.jwtSecretKey, + secret = appConfig.jwtSecretKey, contentName = "foo", - issuer = settings.externalKnoraApiHostPort + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(Some("bar")) } @@ -72,8 +72,8 @@ class JWTHelperSpec extends CoreSpec with ImplicitSender { JWTHelper.extractUserIriFromToken( token = invalidToken, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(None) } @@ -83,8 +83,8 @@ class JWTHelperSpec extends CoreSpec with ImplicitSender { JWTHelper.extractUserIriFromToken( token = tokenWithInvalidSubject, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(None) } @@ -94,8 +94,8 @@ class JWTHelperSpec extends CoreSpec with ImplicitSender { JWTHelper.extractUserIriFromToken( token = tokenWithMissingExp, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(None) } @@ -105,8 +105,8 @@ class JWTHelperSpec extends CoreSpec with ImplicitSender { JWTHelper.extractUserIriFromToken( token = expiredToken, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(None) } @@ -116,8 +116,8 @@ class JWTHelperSpec extends CoreSpec with ImplicitSender { JWTHelper.validateToken( token = tokenWithDifferentIssuer, - secret = settings.jwtSecretKey, - issuer = settings.externalKnoraApiHostPort + secret = appConfig.jwtSecretKey, + issuer = appConfig.knoraApi.externalKnoraApiHostPort ) should be(false) } } From 95540242c397142b3652ee32491fde4643a5df33 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 10:26:19 +0200 Subject: [PATCH 15/44] fix failing tests --- .../knora/webapi/routing/Authenticator.scala | 3 +- .../webapi/routing/v2/ResourcesRouteV2.scala | 14 ++-- .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 2 +- .../webapi/routing/AuthenticatorSpec.scala | 65 +++++++------------ 4 files changed, 32 insertions(+), 52 deletions(-) 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 1dc46c4435..26c39d81ae 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala @@ -462,7 +462,7 @@ object Authenticator extends InstrumentationSupport { * to be valid. * * @param credentials the user supplied and extracted credentials. - * + * @param appConfig the application's configuration * @param system the current [[ActorSystem]] * @return true if the credentials are valid. If the credentials are invalid, then the corresponding exception * will be thrown. @@ -541,7 +541,6 @@ object Authenticator extends InstrumentationSupport { requestContext: RequestContext, appConfig: AppConfig ): Option[KnoraCredentialsV2] = { - // log.debug("extractCredentialsV2 start ...") val credentialsFromParameters: Option[KnoraCredentialsV2] = extractCredentialsFromParametersV2(requestContext) log.debug("extractCredentialsV2 - credentialsFromParameters: {}", credentialsFromParameters) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 70970bb922..f513cbf885 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -23,17 +23,17 @@ import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.messages.util.rdf.JsonLDUtil import org.knora.webapi.messages.v2.responder.resourcemessages._ import org.knora.webapi.messages.v2.responder.searchmessages.SearchResourcesByProjectAndClassRequestV2 +import org.knora.webapi.messages.v2.responder.valuemessages.ArchiveFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.AudioFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.DocumentFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.MovingImageFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.StillImageFileValueContentV2 +import org.knora.webapi.messages.v2.responder.valuemessages.TextFileValueContentV2 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.messages.v2.responder.valuemessages.StillImageFileValueContentV2 -import org.knora.webapi.messages.v2.responder.valuemessages.DocumentFileValueContentV2 -import org.knora.webapi.messages.v2.responder.valuemessages.ArchiveFileValueContentV2 -import org.knora.webapi.messages.v2.responder.valuemessages.TextFileValueContentV2 -import org.knora.webapi.messages.v2.responder.valuemessages.AudioFileValueContentV2 -import org.knora.webapi.messages.v2.responder.valuemessages.MovingImageFileValueContentV2 -import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2 /** * Provides a routing function for API v2 routes that deal with resources. diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index df596b89c9..9f572fd09f 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -25,6 +25,7 @@ import spray.json.JsonParser import java.net.URLEncoder import java.nio.file.Paths import java.time.Instant +import scala.concurrent.Await import scala.concurrent.duration._ import dsp.errors.AssertionException @@ -46,7 +47,6 @@ import org.knora.webapi.routing.v2.OntologiesRouteV2 import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.util._ -import scala.concurrent.Await /** * Tests the API v2 resources route. diff --git a/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala index d57293a29d..e1cc7a63e1 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala @@ -35,8 +35,7 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val getUserByIdentifier = PrivateMethod[Future[UserADM]](Symbol("getUserByIdentifier")) - private val authenticateCredentialsV2 = PrivateMethod[Future[Boolean]](Symbol("authenticateCredentialsV2")) + private val getUserByIdentifier = PrivateMethod[Future[UserADM]](Symbol("getUserByIdentifier")) "During Authentication" when { "called, the 'getUserADMByEmail' method " should { @@ -86,12 +85,9 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT UserIdentifierADM(maybeEmail = Some(AuthenticatorSpec.rootUserEmail)), AuthenticatorSpec.rootUserPassword ) - val resF = Authenticator invokePrivate authenticateCredentialsV2( + val resF = Authenticator.authenticateCredentialsV2( Some(correctPasswordCreds), - appConfig, - system, - appActor, - executionContext + appConfig ) resF map { res => assert(res) @@ -100,12 +96,9 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT "fail with unknown email" in { val wrongPasswordCreds = KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some("wrongemail@example.com")), "wrongpassword") - val resF = Authenticator invokePrivate authenticateCredentialsV2( + val resF = Authenticator.authenticateCredentialsV2( Some(wrongPasswordCreds), - appConfig, - system, - appActor, - executionContext + appConfig ) resF map { _ => assertThrows(BadCredentialsException) @@ -117,12 +110,9 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT UserIdentifierADM(maybeEmail = Some(AuthenticatorSpec.rootUserEmail)), "wrongpassword" ) - val resF = Authenticator invokePrivate authenticateCredentialsV2( + val resF = Authenticator.authenticateCredentialsV2( Some(wrongPasswordCreds), - appConfig, - system, - appActor, - executionContext + appConfig ) resF map { _ => assertThrows(BadCredentialsException) @@ -130,18 +120,15 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT } "succeed with correct token" in { val token = JWTHelper.createToken( - "http://rdfh.ch/users/X-T8IkfQTKa86UWuESpbOA", + "http://rdfh.ch/users/X-T8IkfQTKa86UWuISpbOA", appConfig.jwtSecretKey, appConfig.jwtLongevityAsDuration, appConfig.knoraApi.externalKnoraApiHostPort ) val tokenCreds = KnoraJWTTokenCredentialsV2(token) - val resF = Authenticator invokePrivate authenticateCredentialsV2( + val resF = Authenticator.authenticateCredentialsV2( Some(tokenCreds), - appConfig, - system, - appActor, - executionContext + appConfig ) resF map { res => assert(res) @@ -149,36 +136,30 @@ class AuthenticatorSpec extends CoreSpec with ImplicitSender with PrivateMethodT } "fail with invalidated token" in { val token = JWTHelper.createToken( - "http://rdfh.ch/users/X-T8IkfQTKa86UWuESpbOA", + "http://rdfh.ch/users/X-T8IkfQTKa86UWuISpbOA", appConfig.jwtSecretKey, appConfig.jwtLongevityAsDuration, appConfig.knoraApi.externalKnoraApiHostPort ) val tokenCreds = KnoraJWTTokenCredentialsV2(token) CacheUtil.put(AUTHENTICATION_INVALIDATION_CACHE_NAME, tokenCreds.jwtToken, tokenCreds.jwtToken) - val resF = Authenticator invokePrivate authenticateCredentialsV2( - Some(tokenCreds), - appConfig, - system, - appActor, - executionContext - ) - resF map { _ => - assertThrows(BadCredentialsException) + assertThrows[BadCredentialsException] { + Authenticator.authenticateCredentialsV2( + Some(tokenCreds), + appConfig + ) } + } "fail with wrong token" in { val tokenCreds = KnoraJWTTokenCredentialsV2("123456") - val resF = Authenticator invokePrivate authenticateCredentialsV2( - Some(tokenCreds), - appConfig, - system, - appActor, - executionContext - ) - resF map { _ => - assertThrows(BadCredentialsException) + + assertThrows[BadCredentialsException] { + Authenticator.authenticateCredentialsV2( + Some(tokenCreds), + appConfig + ) } } From ad1e6c165cc65599aa000f9e59dd77b2175d3a33 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 15:11:58 +0200 Subject: [PATCH 16/44] use appConfig for cache definitions --- .../org/knora/webapi/config/AppConfig.scala | 32 +++++++++++++------ .../org/knora/webapi/core/AppServer.scala | 8 ++--- .../scala/org/knora/webapi/CoreSpec.scala | 7 ++-- .../test/scala/org/knora/webapi/E2ESpec.scala | 9 +++--- .../org/knora/webapi/ITKnoraLiveSpec.scala | 11 +++---- .../test/scala/org/knora/webapi/R2RSpec.scala | 1 - 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index e4a4752d07..bf9b47bb83 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -9,6 +9,7 @@ import scala.concurrent.duration import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.util.rdf.RdfFeatureFactory +import org.knora.webapi.util.cache.CacheUtil import typesafe._ import magnolia._ @@ -40,12 +41,22 @@ final case class AppConfig( standoffPerPage: Int, routesToReject: List[String], tmpDatadir: String, - clientTestDataService: ClientTestDataService - // collectClientTestData: Boolean + clientTestDataService: ClientTestDataService, + caches: List[CacheConfig] ) { val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity) val defaultTimeoutAsDuration = scala.concurrent.duration.Duration.apply(defaultTimeout).asInstanceOf[duration.FiniteDuration] + val cacheConfigs: Seq[org.knora.webapi.util.cache.CacheUtil.KnoraCacheConfig] = caches.map { c => + CacheUtil.KnoraCacheConfig( + c.cacheName, + c.maxElementsInMemory, + c.overflowToDisk, + c.eternal, + c.timeToLiveSeconds, + c.timeToIdleSeconds + ) + } } @@ -111,6 +122,15 @@ final case class Sipi( } +final case class CacheConfig( + cacheName: String, + maxElementsInMemory: Int, + overflowToDisk: Boolean, + eternal: Boolean, + timeToLiveSeconds: Int, + timeToIdleSeconds: Int +) + final case class V2( resourcesSequence: ResourcesSequence, graphRoute: GraphRoute, @@ -164,14 +184,6 @@ final case class Triplestore( val gravsearchTimeoutAsDuration = zio.Duration.fromScala(scala.concurrent.duration.Duration(gravsearchTimeout)) } -/** - * Fuseki specific configuration. - * - * @param port - * @param repositoryName - * @param username - * @param password - */ final case class Fuseki( port: Int, repositoryName: String, diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index 7cec85e6f0..5e04952f51 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -32,7 +32,7 @@ final case class AppServer( iiifs: IIIFService, cs: CacheService, hs: HttpServer, - config: AppConfig + appConfig: AppConfig ) { /** @@ -69,7 +69,7 @@ final case class AppServer( _ <- state.set(AppState.CreatingCaches) _ <- ZIO.attempt { CacheUtil.removeAllCaches() - CacheUtil.createCaches(as.settings.caches) + CacheUtil.createCaches(appConfig.cacheConfigs) }.orDie _ <- state.set(AppState.CachesReady) } yield () @@ -128,10 +128,10 @@ final case class AppServer( for { _ <- ZIO.logInfo( - s"DSP-API Server started: ${config.knoraApi.internalKnoraApiBaseUrl}" + s"DSP-API Server started: ${appConfig.knoraApi.internalKnoraApiBaseUrl}" ) - _ = if (config.allowReloadOverHttp) { + _ = if (appConfig.allowReloadOverHttp) { ZIO.logWarning("Resetting DB over HTTP is turned ON") } } yield () diff --git a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala index f35a4019fb..0e9252deb1 100644 --- a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala @@ -82,10 +82,9 @@ abstract class CoreSpec implicit lazy val system: actor.ActorSystem = router.system implicit lazy val executionContext: ExecutionContext = system.dispatcher - // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) - lazy val rdfDataObjects = List.empty[RdfDataObject] - val log: Logger = Logger(this.getClass()) - val appActor = router.ref + lazy val rdfDataObjects = List.empty[RdfDataObject] + val log: Logger = Logger(this.getClass()) + val appActor = router.ref // needed by some tests val cacheServiceSettings = new CacheServiceSettings(system.settings.config) diff --git a/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala index 9d75cac6a4..b6f6691004 100644 --- a/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala @@ -44,7 +44,7 @@ import org.knora.webapi.util.FileUtil import org.knora.webapi.util.LogAspect /** - * This class can be used in End-to-End testing. It starts the Knora-API server + * This class can be used in End-to-End testing. It starts the DSP stack * and provides access to settings and logging. */ abstract class E2ESpec @@ -106,10 +106,9 @@ abstract class E2ESpec implicit lazy val system: akka.actor.ActorSystem = router.system implicit lazy val executionContext: ExecutionContext = system.dispatcher - // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) - lazy val rdfDataObjects = List.empty[RdfDataObject] - val log: Logger = Logger(this.getClass()) - val appActor = router.ref + lazy val rdfDataObjects = List.empty[RdfDataObject] + val log: Logger = Logger(this.getClass()) + val appActor = router.ref // needed by some tests val appConfig = config diff --git a/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala index 99b0f72991..62da336a58 100644 --- a/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala @@ -36,7 +36,7 @@ import org.knora.webapi.testservices.TestClientService import org.knora.webapi.util.LogAspect /** - * This class can be used in End-to-End testing. It starts the Knora server and + * This class can be used in End-to-End testing. It starts the DSP stack and * provides access to settings and logging. */ abstract class ITKnoraLiveSpec @@ -98,11 +98,10 @@ abstract class ITKnoraLiveSpec implicit lazy val system: akka.actor.ActorSystem = router.system implicit lazy val executionContext: ExecutionContext = system.dispatcher - // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) - lazy val rdfDataObjects = List.empty[RdfDataObject] - val log: Logger = Logger(this.getClass()) - val appActor = router.ref - val appConfig = config + lazy val rdfDataObjects = List.empty[RdfDataObject] + val log: Logger = Logger(this.getClass()) + val appActor = router.ref + val appConfig = config // needed by some tests val baseApiUrl = config.knoraApi.internalKnoraApiBaseUrl diff --git a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala index a85984515b..018dcdba11 100644 --- a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala @@ -88,7 +88,6 @@ abstract class R2RSpec } // main difference to other specs (no own systen and executionContext defined) - // implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(system) lazy val rdfDataObjects = List.empty[RdfDataObject] val log: Logger = Logger(this.getClass()) val appActor = router.ref From 04009a2a3142d9ca78fbfd8491ddf99b5055289b Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 15:23:59 +0200 Subject: [PATCH 17/44] remove settings from V1 spec --- .../v1/ResourcesResponderV1Spec.scala | 7 ++-- .../ResourcesResponderV1SpecContextData.scala | 36 ++++++++----------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala index aafddc2761..88bba710ed 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala @@ -641,6 +641,9 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { implicit private val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + private val resourcesResponderV1SpecContextData: ResourcesResponderV1SpecContextData = + new ResourcesResponderV1SpecContextData(appConfig) + private val valueUtilV1 = new ValueUtilV1(appConfig) override lazy val rdfDataObjects = List( @@ -934,7 +937,7 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { val response: JsValue = expectMsgType[ResourceContextResponseV1](timeout).toJsValue - response should be(ResourcesResponderV1SpecContextData.expectedBookResourceContextResponse) + response should be(resourcesResponderV1SpecContextData.expectedBookResourceContextResponse) } "return the context of a page of the book 'Zeitglöcklein des Lebens und Leidens Christi' in the Incunabula test data" in { @@ -948,7 +951,7 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { expectMsgPF(timeout) { case response: ResourceContextResponseV1 => compareResourcePartOfContextResponses( received = response, - expected = ResourcesResponderV1SpecContextData.expectedPageResourceContextResponse + expected = resourcesResponderV1SpecContextData.expectedPageResourceContextResponse ) } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala index 97a442f886..b345403f19 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala @@ -5,22 +5,16 @@ package org.knora.webapi.responders.v1 -import akka.actor.ActorSystem import spray.json.JsValue import spray.json.JsonParser import java.nio.file.Paths import org.knora.webapi.messages.v1.responder.resourcemessages._ -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.util.FileUtil +import org.knora.webapi.config.AppConfig -object ResourcesResponderV1SpecContextData { - - implicit lazy val system: ActorSystem = ActorSystem("webapi") - - val settings: KnoraSettingsImpl = KnoraSettings(system) +final case class ResourcesResponderV1SpecContextData(appConfig: AppConfig) { /* @@ -33,7 +27,7 @@ object ResourcesResponderV1SpecContextData { .readTextFile( Paths.get("..", "test_data/v1/expectedBookContextResponse.json") ) - .replaceAll("IIIF_BASE_URL", settings.externalSipiIIIFGetUrl) + .replaceAll("IIIF_BASE_URL", appConfig.sipi.externalBaseUrl) val expectedBookResourceContextResponse: JsValue = JsonParser(expectedBookResourceContextResponseStr) @@ -51,7 +45,7 @@ object ResourcesResponderV1SpecContextData { locations = None, preview = None, restype_iconsrc = - Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), + Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), restype_description = Some("Diese Resource-Klasse beschreibt ein Buch"), restype_label = Some("Buch"), restype_name = Some("http://www.knora.org/ontology/0803/incunabula#book"), @@ -74,7 +68,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", ny = Some(3505), nx = Some(2613), origname = Some("ad+s167_druck1=0001.tif"), @@ -87,7 +81,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", ny = Some(128), nx = Some(95), origname = Some("ad+s167_druck1=0001.tif"), @@ -97,7 +91,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/82,110/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/82,110/0/default.jpg", ny = Some(110), nx = Some(82), origname = Some("ad+s167_druck1=0001.tif"), @@ -107,7 +101,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/163,219/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/163,219/0/default.jpg", ny = Some(219), nx = Some(163), origname = Some("ad+s167_druck1=0001.tif"), @@ -117,7 +111,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/327,438/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/327,438/0/default.jpg", ny = Some(438), nx = Some(327), origname = Some("ad+s167_druck1=0001.tif"), @@ -127,7 +121,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/653,876/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/653,876/0/default.jpg", ny = Some(876), nx = Some(653), origname = Some("ad+s167_druck1=0001.tif"), @@ -137,8 +131,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = - s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/1307,1753/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/1307,1753/0/default.jpg", ny = Some(1753), nx = Some(1307), origname = Some("ad+s167_druck1=0001.tif"), @@ -148,8 +141,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = - s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", ny = Some(3505), nx = Some(2613), origname = Some("ad+s167_druck1=0001.tif"), @@ -162,7 +154,7 @@ object ResourcesResponderV1SpecContextData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", ny = Some(128), nx = Some(95), origname = Some("ad+s167_druck1=0001.tif"), @@ -170,7 +162,7 @@ object ResourcesResponderV1SpecContextData { ) ), restype_iconsrc = - Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/page.gif"), + Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/page.gif"), restype_description = Some("Eine Seite ist ein Teil eines Buchs"), restype_label = Some("Seite"), restype_name = Some("http://www.knora.org/ontology/0803/incunabula#page"), From 952a32245a82395b6645e527f55727fb7795dd98 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 15:30:02 +0200 Subject: [PATCH 18/44] remove settings from V1 test data --- .../v1/ResourcesResponderV1Spec.scala | 11 ++-- .../v1/ResourcesResponderV1SpecFullData.scala | 63 +++++++++---------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala index 88bba710ed..f170d0be52 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1Spec.scala @@ -644,6 +644,9 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { private val resourcesResponderV1SpecContextData: ResourcesResponderV1SpecContextData = new ResourcesResponderV1SpecContextData(appConfig) + private val resourcesResponderV1SpecFullData: ResourcesResponderV1SpecFullData = + new ResourcesResponderV1SpecFullData(appConfig) + private val valueUtilV1 = new ValueUtilV1(appConfig) override lazy val rdfDataObjects = List( @@ -907,7 +910,7 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { expectMsgPF(timeout) { case response: ResourceFullResponseV1 => compareResourceFullResponses( received = response, - expected = ResourcesResponderV1SpecFullData.expectedBookResourceFullResponse + expected = resourcesResponderV1SpecFullData.expectedBookResourceFullResponse ) } } @@ -922,7 +925,7 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { expectMsgPF(timeout) { case response: ResourceFullResponseV1 => compareResourceFullResponses( received = response, - expected = ResourcesResponderV1SpecFullData.expectedPageResourceFullResponse + expected = resourcesResponderV1SpecFullData.expectedPageResourceFullResponse ) } } @@ -1160,7 +1163,7 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { ) ), resource_reference = Set(nonexistentIri), - mapping = ResourcesResponderV1SpecFullData.dummyMapping, + mapping = resourcesResponderV1SpecFullData.dummyMapping, mappingIri = "http://rdfh.ch/standoff/mappings/StandardMapping" ) @@ -1223,7 +1226,7 @@ class ResourcesResponderV1Spec extends CoreSpec with ImplicitSender { startIndex = 1 ) ), - mapping = ResourcesResponderV1SpecFullData.dummyMapping, + mapping = resourcesResponderV1SpecFullData.dummyMapping, mappingIri = "http://rdfh.ch/standoff/mappings/StandardMapping", resource_reference = Set("http://rdfh.ch/0803/c5058f3a") ) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala index 16ac6ab2b1..f9371d4629 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala @@ -5,21 +5,14 @@ package org.knora.webapi.responders.v1 -import akka.actor.ActorSystem - import org.knora.webapi.messages.v1.responder.resourcemessages._ import org.knora.webapi.messages.v1.responder.valuemessages._ import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.standoffmessages.XMLTag -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.config.AppConfig // FIXME: Rename to something more generic and without spec in the name since it is not a spec and is used in more then one spec -object ResourcesResponderV1SpecFullData { - - implicit lazy val system: ActorSystem = ActorSystem("webapi") - - val settings: KnoraSettingsImpl = KnoraSettings(system) +final case class ResourcesResponderV1SpecFullData(appConfig: AppConfig) { // The expected response to a "full" resource request for a book. val expectedBookResourceFullResponse: ResourceFullResponseV1 = ResourceFullResponseV1( @@ -38,7 +31,7 @@ object ResourcesResponderV1SpecFullData { locations = None, preview = None, restype_iconsrc = - Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "knora-base/link.gif"), + Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "knora-base/link.gif"), restype_description = Some("Verkn\u00FCpfung mehrerer Resourcen"), restype_label = Some("Verkn\u00FCpfungsobjekt"), restype_name = Some("http://www.knora.org/ontology/knora-base#LinkObj"), @@ -64,7 +57,7 @@ object ResourcesResponderV1SpecFullData { locations = None, preview = None, restype_iconsrc = - Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "knora-base/link.gif"), + Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "knora-base/link.gif"), restype_description = Some("Verkn\u00FCpfung mehrerer Resourcen"), restype_label = Some("Verkn\u00FCpfungsobjekt"), restype_name = Some("http://www.knora.org/ontology/knora-base#LinkObj"), @@ -404,7 +397,7 @@ object ResourcesResponderV1SpecFullData { resdata = Some( ResourceDataV1( rights = Some(6), - iconsrc = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), + iconsrc = Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), restype_label = Some("Buch"), restype_name = "http://www.knora.org/ontology/0803/incunabula#book", res_id = "http://rdfh.ch/0803/c5058f3a" @@ -421,7 +414,8 @@ object ResourcesResponderV1SpecFullData { locdata = None, locations = None, preview = None, - restype_iconsrc = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), + restype_iconsrc = + Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), restype_description = Some("Diese Resource-Klasse beschreibt ein Buch"), restype_label = Some("Buch"), restype_name = Some("http://www.knora.org/ontology/0803/incunabula#book"), @@ -445,7 +439,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", ny = Some(128), nx = Some(95), origname = Some("ad+s167_druck1=0001.tif"), @@ -455,7 +449,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/82,110/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/82,110/0/default.jpg", ny = Some(110), nx = Some(82), origname = Some("ad+s167_druck1=0001.tif"), @@ -465,7 +459,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/163,219/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/163,219/0/default.jpg", ny = Some(219), nx = Some(163), origname = Some("ad+s167_druck1=0001.tif"), @@ -475,7 +469,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/327,438/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/327,438/0/default.jpg", ny = Some(438), nx = Some(327), origname = Some("ad+s167_druck1=0001.tif"), @@ -485,7 +479,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/653,876/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/653,876/0/default.jpg", ny = Some(876), nx = Some(653), origname = Some("ad+s167_druck1=0001.tif"), @@ -495,8 +489,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = - s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/1307,1753/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/1307,1753/0/default.jpg", ny = Some(1753), nx = Some(1307), origname = Some("ad+s167_druck1=0001.tif"), @@ -506,8 +499,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = - s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", ny = Some(3505), nx = Some(2613), origname = Some("ad+s167_druck1=0001.tif"), @@ -648,14 +640,14 @@ object ResourcesResponderV1SpecFullData { value_rights = Vector(Some(2)), value_firstprops = Vector(Some("Zeitgl\u00F6cklein des Lebens und Leidens Christi")), value_iconsrcs = - Vector(Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif")), + Vector(Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif")), value_restype = Vector(Some("Buch")), comments = Vector(None), value_ids = Vector("http://rdfh.ch/0803/8a0b1e75/values/ac9ddbf4-62a7-4cdc-b530-16cbbaa265bf"), values = Vector( LinkV1( valueResourceClassIcon = - Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/book.gif"), + Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/book.gif"), valueResourceClassLabel = Some("Buch"), valueResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book"), valueLabel = Some("Zeitgl\u00F6cklein des Lebens und Leidens Christi"), @@ -773,7 +765,7 @@ object ResourcesResponderV1SpecFullData { resdata = Some( ResourceDataV1( rights = Some(6), - iconsrc = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/page.gif"), + iconsrc = Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/page.gif"), restype_label = Some("Seite"), restype_name = "http://www.knora.org/ontology/0803/incunabula#page", res_id = "http://rdfh.ch/0803/8a0b1e75" @@ -792,7 +784,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", ny = Some(3505), nx = Some(2613), origname = Some("ad+s167_druck1=0001.tif"), @@ -805,7 +797,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", ny = Some(128), nx = Some(95), origname = Some("ad+s167_druck1=0001.tif"), @@ -815,7 +807,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/82,110/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/82,110/0/default.jpg", ny = Some(110), nx = Some(82), origname = Some("ad+s167_druck1=0001.tif"), @@ -825,7 +817,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/163,219/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/163,219/0/default.jpg", ny = Some(219), nx = Some(163), origname = Some("ad+s167_druck1=0001.tif"), @@ -835,7 +827,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/327,438/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/327,438/0/default.jpg", ny = Some(438), nx = Some(327), origname = Some("ad+s167_druck1=0001.tif"), @@ -845,7 +837,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/653,876/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/653,876/0/default.jpg", ny = Some(876), nx = Some(653), origname = Some("ad+s167_druck1=0001.tif"), @@ -855,7 +847,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/1307,1753/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/1307,1753/0/default.jpg", ny = Some(1753), nx = Some(1307), origname = Some("ad+s167_druck1=0001.tif"), @@ -865,7 +857,7 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/2613,3505/0/default.jpg", ny = Some(3505), nx = Some(2613), origname = Some("ad+s167_druck1=0001.tif"), @@ -878,14 +870,15 @@ object ResourcesResponderV1SpecFullData { protocol = "file", duration = 0, fps = 0, - path = s"${settings.externalSipiIIIFGetUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", + path = s"${appConfig.sipi.externalBaseUrl}/0803/incunabula_0000000002.jp2/full/95,128/0/default.jpg", ny = Some(128), nx = Some(95), origname = Some("ad+s167_druck1=0001.tif"), format_name = "JPEG2000" ) ), - restype_iconsrc = Some(settings.salsah1BaseUrl + settings.salsah1ProjectIconsBasePath + "incunabula/page.gif"), + restype_iconsrc = + Some(appConfig.salsah1.baseUrl + appConfig.salsah1.projectIconsBasepath + "incunabula/page.gif"), restype_description = Some("Eine Seite ist ein Teil eines Buchs"), restype_label = Some("Seite"), restype_name = Some("http://www.knora.org/ontology/0803/incunabula#page"), From 85d1221788673e2da4a2bb36c4c6cb55e1490deb Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 16:30:11 +0200 Subject: [PATCH 19/44] replace all but one instances of settings --- .../org/knora/webapi/config/AppConfig.scala | 13 +- .../org/knora/webapi/core/ActorSystem.scala | 8 +- .../webapi/core/actors/RoutingActor.scala | 2 +- ...tationReadingGravsearchTypeInspector.scala | 6 +- .../GravsearchTypeInspectionRunner.scala | 12 +- .../types/GravsearchTypeInspector.scala | 11 +- .../InferringGravsearchTypeInspector.scala | 6 +- .../responders/v2/SearchResponderV2.scala | 3 +- .../cache/settings/CacheServiceSettings.scala | 10 +- .../knora/webapi/util/cache/CacheUtil.scala | 2 - .../scala/org/knora/webapi/CoreSpec.scala | 4 +- .../knora/webapi/core/ActorSystemTest.scala | 24 +-- ...searchToCountPrequeryTransformerSpec.scala | 17 +- ...cGravsearchToPrequeryTransformerSpec.scala | 7 +- .../types/GravsearchTypeInspectorSpec.scala | 203 +++++++++++++++--- .../ResourcesResponderV1SpecContextData.scala | 2 +- .../v1/ResourcesResponderV1SpecFullData.scala | 2 +- .../responders/v1/ValuesResponderV1Spec.scala | 4 +- .../webapi/util/cache/CacheUtilSpec.scala | 1 + 19 files changed, 258 insertions(+), 79 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index bf9b47bb83..79021963c5 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -42,7 +42,8 @@ final case class AppConfig( routesToReject: List[String], tmpDatadir: String, clientTestDataService: ClientTestDataService, - caches: List[CacheConfig] + caches: List[CacheConfig], + cacheService: CacheService ) { val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity) val defaultTimeoutAsDuration = @@ -131,6 +132,16 @@ final case class CacheConfig( timeToIdleSeconds: Int ) +final case class CacheService( + enabled: Boolean, + redis: Redis +) + +final case class Redis( + host: String, + port: Int +) + final case class V2( resourcesSequence: ResourcesSequence, graphRoute: GraphRoute, diff --git a/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala b/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala index e004baf46f..df84c43a50 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala @@ -12,14 +12,12 @@ import zio.macros.accessible import scala.concurrent.ExecutionContext import org.knora.webapi.config.AppConfig -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.store.cache.settings.CacheServiceSettings @accessible trait ActorSystem { val system: akka.actor.ActorSystem - val settings: KnoraSettingsImpl + val appConfig: AppConfig val cacheServiceSettings: CacheServiceSettings } @@ -52,8 +50,8 @@ object ActorSystem { actorSystem <- ZIO.acquireRelease(acquire(config, context))(release _) } yield new ActorSystem { override val system: akka.actor.ActorSystem = actorSystem - override val settings: KnoraSettingsImpl = KnoraSettings(actorSystem) - override val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(actorSystem.settings.config) + override val appConfig = config + override val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) } } } 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 386b833f44..8e6b445015 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 @@ -82,7 +82,7 @@ class RoutingActor( /** * The Cache Service's configuration. */ - implicit val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(system.settings.config) + implicit val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) /** * Provides the default global execution context diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala index a73a6d4f8d..888ef0805a 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala @@ -10,6 +10,7 @@ import scala.concurrent.Future import dsp.errors.AssertionException import dsp.errors.GravsearchException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -30,8 +31,9 @@ import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInsp */ class AnnotationReadingGravsearchTypeInspector( nextInspector: Option[GravsearchTypeInspector], - responderData: ResponderData -) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData) { + responderData: ResponderData, + appConfig: AppConfig +) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData, appConfig = appConfig) { /** * Represents a Gravsearch type annotation. 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 b55d619823..9786d41228 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 @@ -11,6 +11,7 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import dsp.errors.GravsearchException +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -21,13 +22,16 @@ import org.knora.webapi.settings.KnoraDispatchers /** * Runs Gravsearch type inspection using one or more type inspector implementations. * + * @param appActor a reference to the application actor * @param responderData the Knora [[ResponderData]]. * @param inferTypes if true, use type inference. + * @param appConfig the application's configuration */ class GravsearchTypeInspectionRunner( appActor: ActorRef, responderData: ResponderData, - inferTypes: Boolean = true + inferTypes: Boolean = true, + appConfig: AppConfig ) { private implicit val executionContext: ExecutionContext = responderData.system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) @@ -38,7 +42,8 @@ class GravsearchTypeInspectionRunner( new InferringGravsearchTypeInspector( nextInspector = None, appActor = appActor, - responderData = responderData + responderData = responderData, + appConfig = appConfig ) ) } else { @@ -48,7 +53,8 @@ class GravsearchTypeInspectionRunner( // The pipeline of type inspectors. private val typeInspectionPipeline = new AnnotationReadingGravsearchTypeInspector( nextInspector = maybeInferringTypeInspector, - responderData = responderData + responderData = responderData, + appConfig = appConfig ) /** 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 dd8c4ce350..c12a0d6b62 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 @@ -11,12 +11,11 @@ import akka.util.Timeout import scala.concurrent.ExecutionContext import scala.concurrent.Future +import org.knora.webapi.config.AppConfig 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 -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl /** * An trait whose implementations can get type information from a parsed Gravsearch query in different ways. @@ -28,14 +27,14 @@ import org.knora.webapi.settings.KnoraSettingsImpl */ abstract class GravsearchTypeInspector( protected val nextInspector: Option[GravsearchTypeInspector], - responderData: ResponderData + responderData: ResponderData, + appConfig: AppConfig ) { - protected val system: ActorSystem = responderData.system - protected val settings: KnoraSettingsImpl = KnoraSettings(system) + protected val system: ActorSystem = responderData.system protected implicit val executionContext: ExecutionContext = system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) - protected implicit val timeout: Timeout = settings.defaultTimeout + protected implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration /** * 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/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala index 695a35764a..a5fd9c8cd7 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala @@ -16,6 +16,7 @@ import scala.concurrent.Future import dsp.errors.AssertionException import dsp.errors.GravsearchException import org.knora.webapi._ +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -35,8 +36,9 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV class InferringGravsearchTypeInspector( nextInspector: Option[GravsearchTypeInspector], appActor: ActorRef, - responderData: ResponderData -) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData) { + responderData: ResponderData, + appConfig: AppConfig +) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData, appConfig = appConfig) { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance 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 7c035e5996..745a7962e1 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 @@ -54,7 +54,8 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) extends ResponderWithStandoffV2(responderData, appConfig) { // A Gravsearch type inspection runner. - private val gravsearchTypeInspectionRunner = new GravsearchTypeInspectionRunner(appActor, responderData) + private val gravsearchTypeInspectionRunner = + new GravsearchTypeInspectionRunner(appActor = appActor, responderData = responderData, appConfig = appConfig) /** * Receives a message of type [[SearchResponderRequestV2]], and returns an appropriate response message. diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/settings/CacheServiceSettings.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/settings/CacheServiceSettings.scala index bfedd49ea8..f8b29cf512 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/settings/CacheServiceSettings.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/settings/CacheServiceSettings.scala @@ -5,13 +5,13 @@ package org.knora.webapi.store.cache.settings -import com.typesafe.config.Config +import org.knora.webapi.config.AppConfig /** * Holds the Cache Service specific settings. */ -class CacheServiceSettings(config: Config) { - val cacheServiceEnabled: Boolean = config.getBoolean("app.cache-service.enabled") - val cacheServiceRedisHost: String = config.getString("app.cache-service.redis.host") - val cacheServiceRedisPort: Int = config.getInt("app.cache-service.redis.port") +class CacheServiceSettings(appConfig: AppConfig) { + val cacheServiceEnabled: Boolean = appConfig.cacheService.enabled + val cacheServiceRedisHost: String = appConfig.cacheService.redis.host + val cacheServiceRedisPort: Int = appConfig.cacheService.redis.port } diff --git a/webapi/src/main/scala/org/knora/webapi/util/cache/CacheUtil.scala b/webapi/src/main/scala/org/knora/webapi/util/cache/CacheUtil.scala index 0be26977a5..0fe2f5c37c 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/cache/CacheUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/cache/CacheUtil.scala @@ -72,7 +72,6 @@ object CacheUtil { def removeAllCaches(): Unit = { val cacheManager = CacheManager.getInstance() cacheManager.removeAllCaches() - // println("CacheUtil: Removed all application caches") } /** @@ -85,7 +84,6 @@ object CacheUtil { Option(cacheManager.getCache(cacheName)) match { case Some(cache) => cache.removeAll() - // println(s"CacheUtil: cleared application cache '$cacheName'") case None => throw ApplicationCacheException(s"Application cache '$cacheName' not found") } diff --git a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala index 0e9252deb1..a940f64b3d 100644 --- a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala @@ -87,9 +87,9 @@ abstract class CoreSpec val appActor = router.ref // needed by some tests - val cacheServiceSettings = new CacheServiceSettings(system.settings.config) - val responderData = ResponderData(system, appActor, cacheServiceSettings) val appConfig = config + val cacheServiceSettings = new CacheServiceSettings(appConfig) + val responderData = ResponderData(system, appActor, cacheServiceSettings) final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ diff --git a/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala b/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala index 98d76ed862..ff75388e61 100644 --- a/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala +++ b/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala @@ -7,20 +7,20 @@ package org.knora.webapi.core import zio._ -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl +import org.knora.webapi.config.AppConfig import org.knora.webapi.store.cache.settings.CacheServiceSettings object ActorSystemTest { - def layer(sys: akka.actor.ActorSystem): ZLayer[Any, Nothing, ActorSystem] = - ZLayer - .succeed( - new ActorSystem { - override val system: akka.actor.ActorSystem = sys - override val settings: KnoraSettingsImpl = KnoraSettings(system) - override val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(system.settings.config) - } - ) - .tap(_ => ZIO.logInfo(">>> ActorSystemTest Initialized <<<")) + def layer(sys: akka.actor.ActorSystem): ZLayer[AppConfig, Nothing, ActorSystem] = + ZLayer.scoped { + for { + config <- ZIO.service[AppConfig] + context <- ZIO.executor.map(_.asExecutionContext) + } yield new ActorSystem { + override val system: akka.actor.ActorSystem = sys + override val appConfig = config + override val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) + } + } } diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala index efb79e2886..7521f86dc6 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala @@ -9,6 +9,7 @@ import scala.concurrent.duration._ import dsp.errors.AssertionException import org.knora.webapi.CoreSpec +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -30,13 +31,19 @@ private object CountQueryHandler { def transformQuery( query: String, appActor: ActorRef, - responderData: ResponderData + responderData: ResponderData, + appConfig: AppConfig )(implicit executionContext: ExecutionContext): SelectQuery = { val constructQuery = GravsearchParser.parseQuery(query) val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val typeInspectionResultFuture = typeInspectionRunner.inspectTypes(constructQuery.whereClause, anythingUser) @@ -340,7 +347,8 @@ class NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec extends Cor CountQueryHandler.transformQuery( inputQueryWithDecimalOptionalSortCriterionAndFilter, appActor, - responderData + responderData, + appConfig ) assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilter) @@ -353,7 +361,8 @@ class NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec extends Cor CountQueryHandler.transformQuery( inputQueryWithDecimalOptionalSortCriterionAndFilterComplex, appActor, - responderData + responderData, + appConfig ) assert(transformedQuery === transformedQueryWithDecimalOptionalSortCriterionAndFilterComplex) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala index e3f2be30f3..bd07ed2711 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala @@ -39,7 +39,12 @@ private object QueryHandler { val constructQuery = GravsearchParser.parseQuery(query) val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val typeInspectionResultFuture = typeInspectionRunner.inspectTypes(constructQuery.whereClause, anythingUser) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala index d72739238d..e10d6634ec 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala @@ -1343,7 +1343,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "The annotation-reading type inspector" should { "get type information from a simple query" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = false) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = false, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithExplicitTypeAnnotations) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1421,7 +1426,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "refine the inspected types for each typeableEntity" in { val typeInspectionRunner = - new InferringGravsearchTypeInspector(nextInspector = None, appActor, responderData = responderData) + new InferringGravsearchTypeInspector( + nextInspector = None, + appActor, + responderData = responderData, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) val (_, entityInfo) = Await.result( typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), @@ -1473,7 +1483,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "sanitize inconsistent resource types that only have knora-base:Resource as base class in common" in { val typeInspectionRunner = - new InferringGravsearchTypeInspector(nextInspector = None, appActor, responderData = responderData) + new InferringGravsearchTypeInspector( + nextInspector = None, + appActor, + responderData = responderData, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) val (usageIndex, entityInfo) = Await.result( typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), @@ -1551,7 +1566,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "sanitize inconsistent resource types that have common base classes other than knora-base:Resource" in { val typeInspectionRunner = - new InferringGravsearchTypeInspector(nextInspector = None, appActor, responderData = responderData) + new InferringGravsearchTypeInspector( + nextInspector = None, + appActor, + responderData = responderData, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes3) val (usageIndex, entityInfo) = Await.result( typeInspectionRunner.getUsageIndexAndEntityInfos(parsedQuery.whereClause, requestingUser = anythingAdminUser), @@ -1613,7 +1633,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "sanitize inconsistent types resulted from a union" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes3) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1673,7 +1698,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { """.stripMargin val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(queryWithOptional) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1735,7 +1765,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the most specific type from redundant ones given in a query" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithRedundantTypes) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1746,7 +1781,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer that an entity is a knora-api:Resource if there is an rdf:type statement about it and the specified type is a Knora resource class" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1761,7 +1801,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer a property's knora-api:objectType if the property's IRI is used as a predicate" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryKnoraObjectTypeFromPropertyIriRule) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1771,7 +1816,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer an entity's type if the entity is used as the object of a statement and the predicate's knora-api:objectType is known" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfObjectFromPropertyRule) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1781,7 +1831,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the knora-api:objectType of a property variable if it's used with an object whose type is known" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryKnoraObjectTypeFromObjectRule) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1791,7 +1846,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer an entity's type if the entity is used as the subject of a statement, the predicate is an IRI, and the predicate's knora-api:subjectType is known" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfSubjectFromPropertyRule) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1801,7 +1861,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the knora-api:objectType of a property variable if it's compared to a known property IRI in a FILTER" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryPropertyVarTypeFromFilterRule) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1811,7 +1876,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a non-property variable if it's compared to an XSD literal in a FILTER" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryNonPropertyVarTypeFromFilterRule) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1821,7 +1891,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a non-property variable used as the argument of a function in a FILTER" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryVarTypeFromFunction) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1831,7 +1906,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a non-property IRI used as the argument of a function in a FILTER" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryIriTypeFromFunction) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1841,7 +1921,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the types in a query that requires 6 iterations" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(PathologicalQuery) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1851,7 +1936,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "know the object type of rdfs:label" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithRdfsLabelAndLiteral) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1861,7 +1951,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a variable used as the object of rdfs:label" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithRdfsLabelAndVariable) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1871,7 +1966,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a variable when it is compared with another variable in a FILTER (in the simple schema)" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourcesInSimpleSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1881,7 +1981,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a variable when it is compared with another variable in a FILTER (in the complex schema)" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourcesInComplexSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1891,7 +1996,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a resource IRI when it is compared with a variable in a FILTER (in the simple schema)" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourceIriInSimpleSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1901,7 +2011,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer the type of a resource IRI when it is compared with a variable in a FILTER (in the complex schema)" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourceIriInComplexSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1911,7 +2026,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "infer knora-api:Resource as the subject type of a subproperty of knora-api:hasLinkTo" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithFilterComparison) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1921,7 +2041,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "reject a query with a non-Knora property whose type cannot be inferred" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithoutAnnotation) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1932,7 +2057,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "accept a query with a non-Knora property whose type can be inferred" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithAnnotation) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1942,7 +2072,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "ignore Gravsearch options" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithGravsearchOptions) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1952,7 +2087,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "reject a query with inconsistent types inferred from statements" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes1) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) @@ -1963,7 +2103,12 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { "reject a query with inconsistent types inferred from a FILTER" in { val typeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor, responderData = responderData, inferTypes = true) + new GravsearchTypeInspectionRunner( + appActor, + responderData = responderData, + inferTypes = true, + appConfig = appConfig + ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes2) val resultFuture: Future[GravsearchTypeInspectionResult] = typeInspectionRunner.inspectTypes(parsedQuery.whereClause, requestingUser = anythingAdminUser) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala index b345403f19..6b43b87bf0 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecContextData.scala @@ -10,9 +10,9 @@ import spray.json.JsonParser import java.nio.file.Paths +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.resourcemessages._ import org.knora.webapi.util.FileUtil -import org.knora.webapi.config.AppConfig final case class ResourcesResponderV1SpecContextData(appConfig: AppConfig) { diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala index f9371d4629..0990bfeba8 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ResourcesResponderV1SpecFullData.scala @@ -5,11 +5,11 @@ package org.knora.webapi.responders.v1 +import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.resourcemessages._ import org.knora.webapi.messages.v1.responder.valuemessages._ import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.standoffmessages.XMLTag -import org.knora.webapi.config.AppConfig // FIXME: Rename to something more generic and without spec in the name since it is not a spec and is used in more then one spec final case class ResourcesResponderV1SpecFullData(appConfig: AppConfig) { diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v1/ValuesResponderV1Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v1/ValuesResponderV1Spec.scala index 1fec7a5056..fbe3b56199 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v1/ValuesResponderV1Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v1/ValuesResponderV1Spec.scala @@ -40,6 +40,7 @@ object ValuesResponderV1Spec { private val imagesUser = SharedTestDataADM.imagesUser01 private val anythingUser = SharedTestDataADM.anythingUser1 + } /** @@ -47,6 +48,7 @@ object ValuesResponderV1Spec { */ class ValuesResponderV1Spec extends CoreSpec with ImplicitSender { implicit private val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance + private val resourcesResponderV1SpecFullData = ResourcesResponderV1SpecFullData(appConfig) import ValuesResponderV1Spec._ @@ -2067,7 +2069,7 @@ class ValuesResponderV1Spec extends CoreSpec with ImplicitSender { ) ), resource_reference = Set(nonexistentIri), - mapping = ResourcesResponderV1SpecFullData.dummyMapping, + mapping = resourcesResponderV1SpecFullData.dummyMapping, mappingIri = "http://rdfh.ch/standoff/mappings/StandardMapping" ) diff --git a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala index fbb0ea86c8..dc98bc4929 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala @@ -35,6 +35,7 @@ class CacheUtilSpec final override def beforeAll(): Unit = { CacheUtil.removeAllCaches() CacheUtil.createCaches(settings.caches) + // CacheUtil.createCaches(appConfig.cacheConfigs) } final override def afterAll(): Unit = { From 94976168581f2e898877400285c2653c68adacb8 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 16:49:00 +0200 Subject: [PATCH 20/44] refactor CacheUtilSpec to remove settings --- .../knora/webapi/settings/KnoraSettings.scala | 276 ------------------ .../webapi/util/cache/CacheUtilSpec.scala | 40 +-- 2 files changed, 11 insertions(+), 305 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala diff --git a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala deleted file mode 100644 index 202e15d235..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 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.settings - -import akka.ConfigurationException -import akka.actor.ActorSystem -import akka.actor.ExtendedActorSystem -import akka.actor.Extension -import akka.actor.ExtensionId -import akka.actor.ExtensionIdProvider -import com.typesafe.config.Config -import com.typesafe.config.ConfigValue -import com.typesafe.scalalogging.Logger - -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import scala.concurrent.duration._ -import scala.jdk.CollectionConverters._ - -import dsp.errors.FileWriteException -import dsp.valueobjects.User -import org.knora.webapi.util.cache.CacheUtil.KnoraCacheConfig - -/** - * Reads application settings that come from `application.conf`. - */ -class KnoraSettingsImpl(config: Config, log: Logger) extends Extension { - - // print config - val printExtendedConfig: Boolean = config.getBoolean("app.print-extended-config") - - // used for communication inside the knora stack - val internalKnoraApiHost: String = config.getString("app.knora-api.internal-host") - val internalKnoraApiPort: Int = config.getInt("app.knora-api.internal-port") - val internalKnoraApiBaseUrl: String = - "http://" + internalKnoraApiHost + (if (internalKnoraApiPort != 80) - ":" + internalKnoraApiPort - else "") - - // used for communication between the outside and the knora stack, e.g., browser - val externalKnoraApiProtocol: String = config.getString("app.knora-api.external-protocol") - val externalKnoraApiHost: String = config.getString("app.knora-api.external-host") - val externalKnoraApiPort: Int = config.getInt("app.knora-api.external-port") - val externalKnoraApiHostPort: String = externalKnoraApiHost + (if (externalKnoraApiPort != 80) - ":" + externalKnoraApiPort - else "") - val externalKnoraApiBaseUrl: String = - externalKnoraApiProtocol + "://" + externalKnoraApiHost + (if (externalKnoraApiPort != 80) - ":" + externalKnoraApiPort - else "") - - /** - * If the external hostname is localhost or 0.0.0.0, include the configured - * external port number in ontology IRIs for manual testing. - */ - val externalOntologyIriHostAndPort: String = - if (externalKnoraApiHost == "0.0.0.0" || externalKnoraApiHost == "localhost") { - externalKnoraApiHostPort - } else { - // Otherwise, don't include any port number in IRIs, so the IRIs will work both with http - // and with https. - externalKnoraApiHost - } - - val salsah1BaseUrl: String = config.getString("app.salsah1.base-url") - val salsah1ProjectIconsBasePath: String = config.getString("app.salsah1.project-icons-basepath") - - val tmpDataDir: String = config.getString("app.tmp-datadir") - val dataDir: String = config.getString("app.datadir") - - // try to create the directories - if (!Files.exists(Paths.get(tmpDataDir))) { - try { - Files.createDirectories(Paths.get(tmpDataDir)) - } catch { - case e: Throwable => - throw FileWriteException(s"Tmp data directory $tmpDataDir could not be created: ${e.getMessage}") - } - } - - // try to create the directories - if (!Files.exists(Paths.get(dataDir))) { - try { - Files.createDirectories(Paths.get(dataDir)) - } catch { - case e: Throwable => - throw FileWriteException(s"Tmp data directory $tmpDataDir could not be created: ${e.getMessage}") - } - } - - val imageMimeTypes: Set[String] = config - .getList("app.sipi.image-mime-types") - .iterator - .asScala - .map { mType: ConfigValue => - mType.unwrapped.toString - } - .toSet - - val documentMimeTypes: Set[String] = config - .getList("app.sipi.document-mime-types") - .iterator - .asScala - .map { mType: ConfigValue => - mType.unwrapped.toString - } - .toSet - - val textMimeTypes: Set[String] = config - .getList("app.sipi.text-mime-types") - .iterator - .asScala - .map { mType: ConfigValue => - mType.unwrapped.toString - } - .toSet - - val audioMimeTypes: Set[String] = config - .getList("app.sipi.audio-mime-types") - .iterator - .asScala - .map { mType: ConfigValue => - mType.unwrapped.toString - } - .toSet - - val videoMimeTypes: Set[String] = config - .getList("app.sipi.video-mime-types") - .iterator - .asScala - .map { mType: ConfigValue => - mType.unwrapped.toString - } - .toSet - - val archiveMimeTypes: Set[String] = config - .getList("app.sipi.archive-mime-types") - .iterator - .asScala - .map { mType: ConfigValue => - mType.unwrapped.toString - } - .toSet - - val internalSipiProtocol: String = config.getString("app.sipi.internal-protocol") - val internalSipiHost: String = config.getString("app.sipi.internal-host") - val internalSipiPort: Int = config.getInt("app.sipi.internal-port") - val internalSipiBaseUrl: String = internalSipiProtocol + "://" + internalSipiHost + (if (internalSipiPort != 80) - ":" + internalSipiPort - else "") - - val sipiTimeout: FiniteDuration = getFiniteDuration("app.sipi.timeout", config) - - val externalSipiProtocol: String = config.getString("app.sipi.external-protocol") - val externalSipiHost: String = config.getString("app.sipi.external-host") - val externalSipiPort: Int = config.getInt("app.sipi.external-port") - val externalSipiBaseUrl: String = externalSipiProtocol + "://" + externalSipiHost + (if (externalSipiPort != 80) - ":" + externalSipiPort - else "") - val sipiFileServerPrefix: String = config.getString("app.sipi.file-server-path") - val externalSipiIIIFGetUrl: String = externalSipiBaseUrl - val sipiFileMetadataRouteV2: String = config.getString("app.sipi.file-metadata-route") - val sipiMoveFileRouteV2: String = config.getString("app.sipi.move-file-route") - val sipiDeleteTempFileRouteV2: String = config.getString("app.sipi.delete-temp-file-route") - - val arkResolver: String = config.getString("app.ark.resolver") - val arkAssignedNumber: Int = config.getInt("app.ark.assigned-number") - - val caches: Vector[KnoraCacheConfig] = config - .getList("app.caches") - .iterator - .asScala - .map { cacheConfigItem: ConfigValue => - val cacheConfigMap = cacheConfigItem.unwrapped.asInstanceOf[java.util.HashMap[String, Any]].asScala - KnoraCacheConfig( - cacheConfigMap("cache-name").asInstanceOf[String], - cacheConfigMap("max-elements-in-memory").asInstanceOf[Int], - cacheConfigMap("overflow-to-disk").asInstanceOf[Boolean], - cacheConfigMap("eternal").asInstanceOf[Boolean], - cacheConfigMap("time-to-live-seconds").asInstanceOf[Int], - cacheConfigMap("time-to-idle-seconds").asInstanceOf[Int] - ) - } - .toVector - - val defaultTimeout: FiniteDuration = getFiniteDuration("app.default-timeout", config) - - val dumpMessages: Boolean = config.getBoolean("app.dump-messages") - val showInternalErrors: Boolean = config.getBoolean("app.show-internal-errors") - val maxResultsPerSearchResultPage: Int = config.getInt("app.max-results-per-search-result-page") - val standoffPerPage: Int = config.getInt("app.standoff-per-page") - val defaultIconSizeDimX: Int = config.getInt("app.gui.default-icon-size.dim-x") - val defaultIconSizeDimY: Int = config.getInt("app.gui.default-icon-size.dim-y") - - val v2ResultsPerPage: Int = config.getInt("app.v2.resources-sequence.results-per-page") - val searchValueMinLength: Int = config.getInt("app.v2.fulltext-search.search-value-min-length") - - val defaultGraphDepth: Int = config.getInt("app.v2.graph-route.default-graph-depth") - val maxGraphDepth: Int = config.getInt("app.v2.graph-route.max-graph-depth") - val maxGraphBreadth: Int = config.getInt("app.v2.graph-route.max-graph-breadth") - - val triplestoreType: String = config.getString("app.triplestore.dbtype") - val triplestoreHost: String = config.getString("app.triplestore.host") - - val triplestoreUseHttps: Boolean = config.getBoolean("app.triplestore.use-https") - - val triplestoreAutoInit: Boolean = config.getBoolean("app.triplestore.auto-init") - - val triplestorePort: Int = config.getInt("app.triplestore.fuseki.port") - val triplestoreDatabaseName: String = config.getString("app.triplestore.fuseki.repository-name") - val triplestoreUsername: String = config.getString("app.triplestore.fuseki.username") - val triplestorePassword: String = config.getString("app.triplestore.fuseki.password") - - // used in the store package - val tripleStoreConfig: Config = config.getConfig("app.triplestore") - - val jwtSecretKey: String = config.getString("app.jwt-secret-key") - val jwtLongevity: FiniteDuration = getFiniteDuration("app.jwt-longevity", config) - - val cookieDomain: String = config.getString("app.cookie-domain") - - val fallbackLanguage: String = config.getString("user.default-language") - - val profileQueries: Boolean = config.getBoolean("app.triplestore.profile-queries") - - val routesToReject: Seq[String] = config - .getList("app.routes-to-reject") - .iterator - .asScala - .map { mType: ConfigValue => - mType.unwrapped.toString - } - .toSeq - - val allowReloadOverHTTP: Boolean = config.getBoolean("app.allow-reload-over-http") - - val bcryptPasswordStrength = - User.PasswordStrength - .make(config.getInt("app.bcrypt-password-strength")) - .fold(e => throw new ConfigurationException(e.head), v => v) - - // Client test data service - - val collectClientTestData: Boolean = if (config.hasPath("app.client-test-data-service.collect-client-test-data")) { - config.getBoolean("app.client-test-data-service.collect-client-test-data") - } else { - false - } - - private def getFiniteDuration(path: String, underlying: Config): FiniteDuration = - Duration(underlying.getString(path)) match { - case x: FiniteDuration => x - case _ => throw new ConfigurationException(s"Config setting '$path' must be a finite duration") - } - - val prometheusEndpoint: Boolean = config.getBoolean("app.monitoring.prometheus-endpoint") - - val shaclShapesDir: Path = Paths.get(config.getString("app.shacl.shapes-dir")) -} - -object KnoraSettings extends ExtensionId[KnoraSettingsImpl] with ExtensionIdProvider { - - override def lookup: KnoraSettings.type = KnoraSettings - - override def createExtension(system: ExtendedActorSystem) = - new KnoraSettingsImpl(system.settings.config, Logger(this.getClass)) - - /** - * Java API: retrieve the Settings extension for the given system. - */ - override def get(system: ActorSystem): KnoraSettingsImpl = super.get(system) -} diff --git a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala index dc98bc4929..b3150648a7 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala @@ -5,58 +5,40 @@ package org.knora.webapi.util.cache -import akka.actor.ActorSystem import akka.testkit.TestKit -import com.typesafe.scalalogging.LazyLogging -import org.scalatest.BeforeAndAfterAll -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike -import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.CoreSpec import org.knora.webapi.routing.Authenticator -import org.knora.webapi.settings.KnoraSettings -import org.knora.webapi.settings.KnoraSettingsImpl import org.knora.webapi.sharedtestdata.SharedTestDataV1 -class CacheUtilSpec - extends TestKit(ActorSystem("CacheUtilSpec")) - with AnyWordSpecLike - with Matchers - with BeforeAndAfterAll - with LazyLogging - with Authenticator { - - StringFormatter.initForTest() - val settings: KnoraSettingsImpl = KnoraSettings(system) +class CacheUtilSpec extends CoreSpec { private val cacheName = Authenticator.AUTHENTICATION_INVALIDATION_CACHE_NAME private val sessionId = System.currentTimeMillis().toString - final override def beforeAll(): Unit = { - CacheUtil.removeAllCaches() - CacheUtil.createCaches(settings.caches) - // CacheUtil.createCaches(appConfig.cacheConfigs) - } - - final override def afterAll(): Unit = { - CacheUtil.removeAllCaches() - TestKit.shutdownActorSystem(system) - } - "Caching" should { "allow to set and get the value " in { + CacheUtil.removeAllCaches() + CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.put(cacheName, sessionId, SharedTestDataV1.rootUser) CacheUtil.get(cacheName, sessionId) should be(Some(SharedTestDataV1.rootUser)) + TestKit.shutdownActorSystem(system) } "return none if key is not found " in { + CacheUtil.removeAllCaches() + CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.get(cacheName, 213.toString) should be(None) + TestKit.shutdownActorSystem(system) } "allow to delete a set value " in { + CacheUtil.removeAllCaches() + CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.remove(cacheName, sessionId) CacheUtil.get(cacheName, sessionId) should be(None) + TestKit.shutdownActorSystem(system) } } } From f27a8de520c564c2803036ffee7dc7a18b8510d4 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 17:52:16 +0200 Subject: [PATCH 21/44] add missing things and clean-up --- webapi/src/main/resources/application.conf | 8 -- .../org/knora/webapi/config/AppConfig.scala | 109 ++++++++++-------- webapi/src/test/resources/test.conf | 15 +-- 3 files changed, 72 insertions(+), 60 deletions(-) diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index d0b5ba0542..483af01453 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -306,7 +306,6 @@ app { file-server-path = "server" - file-metadata-route = "knora.json" move-file-route = "store" delete-temp-file-route = "delete_temp_file" @@ -442,7 +441,6 @@ app { triplestore { dbtype = "fuseki" dbtype = ${?KNORA_WEBAPI_TRIPLESTORE_DBTYPE} - // dbtype = "fake-triplestore" use-https = false use-https = ${?KNORA_WEBAPI_TRIPLESTORE_USE_HTTPS} @@ -471,8 +469,6 @@ app { password = ${?KNORA_WEBAPI_TRIPLESTORE_FUSEKI_PASSWORD} } - reload-on-start = false // ignored if "memory" as it will always reload - // If true, the time taken by each SPARQL query is logged at DEBUG level. To see these messages, // set loglevel = "DEBUG" above, and // @@ -505,10 +501,6 @@ app { } } -user { - default-language: "en" -} - kamon.prometheus.embedded-server { # Hostname and port used by the embedded web server to publish the diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 79021963c5..1d76545f4b 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -10,7 +10,8 @@ import scala.concurrent.duration import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.util.rdf.RdfFeatureFactory import org.knora.webapi.util.cache.CacheUtil - +import java.nio.file.Files +import dsp.errors.FileWriteException import typesafe._ import magnolia._ @@ -28,22 +29,23 @@ final case class AppConfig( jwtLongevity: String, cookieDomain: String, allowReloadOverHttp: Boolean, - knoraApi: KnoraAPI, + fallbackLanguage: String, + knoraApi: KnoraApi, sipi: Sipi, ark: Ark, salsah1: Salsah1, - gui: Gui, - triplestore: Triplestore, - v2: V2, - shacl: Shacl, - fallbackLanguage: String, + caches: List[CacheConfig], + tmpDatadir: String, + datadir: String, maxResultsPerSearchResultPage: Int, standoffPerPage: Int, + v2: V2, + gui: Gui, routesToReject: List[String], - tmpDatadir: String, - clientTestDataService: ClientTestDataService, - caches: List[CacheConfig], - cacheService: CacheService + triplestore: Triplestore, + shacl: Shacl, + cacheService: CacheService, + clientTestDataService: ClientTestDataService ) { val jwtLongevityAsDuration = scala.concurrent.duration.Duration(jwtLongevity) val defaultTimeoutAsDuration = @@ -59,18 +61,35 @@ final case class AppConfig( ) } + // try to create the directories + if (!Files.exists(Paths.get(tmpDatadir))) { + try { + Files.createDirectories(Paths.get(tmpDatadir)) + } catch { + case e: Throwable => + throw FileWriteException(s"Tmp data directory $tmpDatadir could not be created: ${e.getMessage}") + } + } + + // try to create the directories + if (!Files.exists(Paths.get(datadir))) { + try { + Files.createDirectories(Paths.get(datadir)) + } catch { + case e: Throwable => + throw FileWriteException(s"Data directory $datadir could not be created: ${e.getMessage}") + } + } + } -final case class KnoraAPI( +final case class KnoraApi( internalHost: String, internalPort: Int, externalProtocol: String, externalHost: String, externalPort: Int ) { - val internalKnoraApiHostPort: String = internalHost + (if (internalPort != 80) - ":" + internalPort - else "") val internalKnoraApiBaseUrl: String = "http://" + internalHost + (if (internalPort != 80) ":" + internalPort else "") @@ -113,16 +132,26 @@ final case class Sipi( audioMimeTypes: List[String], archiveMimeTypes: List[String] ) { - def internalBaseUrl: String = "http://" + internalHost + (if (internalPort != 80) + val internalBaseUrl: String = "http://" + internalHost + (if (internalPort != 80) ":" + internalPort else "") - def externalBaseUrl: String = "http://" + externalHost + (if (externalPort != 80) + val externalBaseUrl: String = "http://" + externalHost + (if (externalPort != 80) ":" + externalPort else "") val timeoutInSeconds: duration.Duration = scala.concurrent.duration.Duration(timeout) } +final case class Ark( + resolver: String, + assignedNumber: Int +) + +final case class Salsah1( + baseUrl: String, + projectIconsBasepath: String +) + final case class CacheConfig( cacheName: String, maxElementsInMemory: Int, @@ -132,39 +161,24 @@ final case class CacheConfig( timeToIdleSeconds: Int ) -final case class CacheService( - enabled: Boolean, - redis: Redis -) - -final case class Redis( - host: String, - port: Int -) - final case class V2( resourcesSequence: ResourcesSequence, - graphRoute: GraphRoute, - fulltextSearch: FulltextSearch + fulltextSearch: FulltextSearch, + graphRoute: GraphRoute ) final case class ResourcesSequence( resultsPerPage: Int ) -final case class GraphRoute( - defaultGraphDepth: Int, - maxGraphBreadth: Int, - maxGraphDepth: Int -) - final case class FulltextSearch( searchValueMinLength: Int ) -final case class Salsah1( - baseUrl: String, - projectIconsBasepath: String +final case class GraphRoute( + defaultGraphDepth: Int, + maxGraphDepth: Int, + maxGraphBreadth: Int ) final case class Gui( @@ -176,11 +190,6 @@ final case class DefaultIconSize( dimY: Int ) -final case class Ark( - resolver: String, - assignedNumber: Int -) - final case class Triplestore( dbtype: String, useHttps: Boolean, @@ -188,8 +197,8 @@ final case class Triplestore( queryTimeout: String, gravsearchTimeout: String, autoInit: Boolean, - profileQueries: Boolean, - fuseki: Fuseki + fuseki: Fuseki, + profileQueries: Boolean ) { val queryTimeoutAsDuration = zio.Duration.fromScala(scala.concurrent.duration.Duration(queryTimeout)) val gravsearchTimeoutAsDuration = zio.Duration.fromScala(scala.concurrent.duration.Duration(gravsearchTimeout)) @@ -208,6 +217,16 @@ final case class Shacl( val shapesDirPath = Paths.get(shapesDir) } +final case class CacheService( + enabled: Boolean, + redis: Redis +) + +final case class Redis( + host: String, + port: Int +) + final case class ClientTestDataService( collectClientTestData: Boolean ) diff --git a/webapi/src/test/resources/test.conf b/webapi/src/test/resources/test.conf index 577fd748e8..67af9a8283 100644 --- a/webapi/src/test/resources/test.conf +++ b/webapi/src/test/resources/test.conf @@ -2,7 +2,6 @@ include "application" akka { log-config-on-start = false - //loggers = ["akka.testkit.TestEventListener"] loggers = ["akka.event.slf4j.Slf4jLogger"] logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" loglevel = "ERROR" @@ -29,17 +28,19 @@ akka { app { testing = true - print-short-config = false - triplestore.auto-init = true allow-reload-over-http = true - client-test-data-service { - # If true, collect client test data from E2E tests. - collect-client-test-data = false - collect-client-test-data = ${?KNORA_WEBAPI_COLLECT_CLIENT_TEST_DATA} + triplestore { + auto-init = true } shacl { shapes-dir = "../test_data/shacl" } + + client-test-data-service { + # If true, collect client test data from E2E tests. + collect-client-test-data = false + collect-client-test-data = ${?KNORA_WEBAPI_COLLECT_CLIENT_TEST_DATA} + } } From ee9b42f13a84fe9e7b0c59403b41d2f6ea121453 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Wed, 14 Sep 2022 18:38:04 +0200 Subject: [PATCH 22/44] add missing config --- webapi/src/main/resources/application.conf | 4 ++++ .../main/scala/org/knora/webapi/config/AppConfig.scala | 5 +++-- .../src/main/scala/org/knora/webapi/core/AppServer.scala | 8 ++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index 483af01453..0afd07e262 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -495,6 +495,10 @@ app { } } + client-test-data-service { + collect-client-test-data = false + } + monitoring { prometheus-endpoint: false prometheus-endpoint: ${?KNORA_WEBAPI_PROMETHEUS_ENDPOINT} diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 1d76545f4b..45bff3ed0d 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -4,14 +4,15 @@ import com.typesafe.config.ConfigFactory import zio._ import zio.config._ +import java.nio.file.Files import java.nio.file.Paths import scala.concurrent.duration +import dsp.errors.FileWriteException import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.util.rdf.RdfFeatureFactory import org.knora.webapi.util.cache.CacheUtil -import java.nio.file.Files -import dsp.errors.FileWriteException + import typesafe._ import magnolia._ diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index 5e04952f51..ec56345cc2 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -174,7 +174,7 @@ object AppServer { with AppConfig with State - private def startup( + def startup( requiresRepository: Boolean, requiresIIIFService: Boolean ): ZIO[AppServerEnvironment, Nothing, Unit] = @@ -194,7 +194,7 @@ object AppServer { /* Live version */ val live: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { - startup(true, true) + startup(requiresRepository = true, requiresIIIFService = true) } /** @@ -203,7 +203,7 @@ object AppServer { */ val testWithSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { - startup(false, true) + startup(requiresRepository = false, requiresIIIFService = true) } /** @@ -212,6 +212,6 @@ object AppServer { */ val testWithoutSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { - startup(false, false) + startup(requiresRepository = false, requiresIIIFService = false) } } From 58788360593c134b4661ded6c518a5ee664b706a Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Thu, 15 Sep 2022 08:20:10 +0200 Subject: [PATCH 23/44] reformat --- webapi/src/main/scala/org/knora/webapi/Main.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index f8d221ec89..39944be042 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -2,7 +2,6 @@ * 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 import zio._ From 36e77b7285c18a6de03edac8c37811b636072562 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Thu, 15 Sep 2022 08:54:33 +0200 Subject: [PATCH 24/44] use Try instead of try catch --- .../org/knora/webapi/config/AppConfig.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 45bff3ed0d..50c4d244a9 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -7,6 +7,7 @@ import zio.config._ import java.nio.file.Files import java.nio.file.Paths import scala.concurrent.duration +import scala.util.Try import dsp.errors.FileWriteException import org.knora.webapi.messages.StringFormatter @@ -62,24 +63,23 @@ final case class AppConfig( ) } - // try to create the directories + // create the directories if (!Files.exists(Paths.get(tmpDatadir))) { - try { + val tmpDataDirCreation = Try { Files.createDirectories(Paths.get(tmpDatadir)) - } catch { - case e: Throwable => - throw FileWriteException(s"Tmp data directory $tmpDatadir could not be created: ${e.getMessage}") - } + }.fold( + e => throw FileWriteException(s"Tmp data directory $tmpDatadir could not be created: ${e.getMessage}"), + v => ZIO.logInfo(s"Created tmp directory $tmpDatadir") + ) } - // try to create the directories if (!Files.exists(Paths.get(datadir))) { - try { + val dataDirCreation = Try { Files.createDirectories(Paths.get(datadir)) - } catch { - case e: Throwable => - throw FileWriteException(s"Data directory $datadir could not be created: ${e.getMessage}") - } + }.fold( + e => throw FileWriteException(s"Data directory $datadir could not be created: ${e.getMessage}"), + v => ZIO.logInfo(s"Created directory $datadir") + ) } } From 94a0e1ef27d10096257196baf18e9c69d8412b00 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Thu, 15 Sep 2022 13:35:20 +0200 Subject: [PATCH 25/44] clean up --- .../main/scala/org/knora/webapi/Main.scala | 9 +++-- .../org/knora/webapi/config/AppConfig.scala | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index 39944be042..1d06439376 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -11,6 +11,8 @@ import org.knora.webapi.core.AppServer object Main extends ZIOApp { + override def environmentTag: EnvironmentTag[Environment] = EnvironmentTag[Environment] + /** * The `Environment` that we require to exist at startup. */ @@ -20,17 +22,14 @@ object Main extends ZIOApp { * `Bootstrap` will ensure that everything is instantiated when the Runtime is created * and cleaned up when the Runtime is shutdown. */ - override val bootstrap: ZLayer[ + override def bootstrap: ZLayer[ ZIOAppArgs with Scope, Any, Environment ] = ZLayer.empty ++ Runtime.removeDefaultLoggers ++ SLF4J.slf4j ++ core.LayersLive.dspLayersLive - /* Needed for ZIO type magic */ - override val environmentTag: EnvironmentTag[Environment] = EnvironmentTag[Environment] - /* Here we start our Application */ - override val run = + override def run = (for { never <- ZIO.never } yield never).provideLayer(AppServer.live) diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 50c4d244a9..a6a0aa8c4c 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -16,9 +16,12 @@ import org.knora.webapi.util.cache.CacheUtil import typesafe._ import magnolia._ +import scala.util.Success +import scala.util.Failure +import java.nio.file.Path /** - * Represents (eventually) the complete configuration as defined in application.conf. + * Represents the configuration as defined in application.conf. */ final case class AppConfig( testing: Boolean = false, @@ -64,22 +67,20 @@ final case class AppConfig( } // create the directories - if (!Files.exists(Paths.get(tmpDatadir))) { - val tmpDataDirCreation = Try { - Files.createDirectories(Paths.get(tmpDatadir)) - }.fold( - e => throw FileWriteException(s"Tmp data directory $tmpDatadir could not be created: ${e.getMessage}"), - v => ZIO.logInfo(s"Created tmp directory $tmpDatadir") - ) + val tmpDataDirCreation: Try[Path] = Try { + Files.createDirectories(Paths.get(tmpDatadir)) + } + tmpDataDirCreation match { + case Success(_) => ZIO.logInfo(s"Created tmp directory $tmpDatadir") + case Failure(e) => throw FileWriteException(s"Tmp data directory $tmpDatadir could not be created: ${e.getMessage}") } - if (!Files.exists(Paths.get(datadir))) { - val dataDirCreation = Try { - Files.createDirectories(Paths.get(datadir)) - }.fold( - e => throw FileWriteException(s"Data directory $datadir could not be created: ${e.getMessage}"), - v => ZIO.logInfo(s"Created directory $datadir") - ) + val dataDirCreation: Try[Path] = Try { + Files.createDirectories(Paths.get(datadir)) + } + dataDirCreation match { + case Success(_) => ZIO.logInfo(s"Created directory $datadir") + case Failure(e) => throw FileWriteException(s"Data directory $datadir could not be created: ${e.getMessage}") } } @@ -251,7 +252,7 @@ object AppConfig { private val config: IO[ReadError[String], AppConfig] = read(descriptor[AppConfig].mapKey(toKebabCase) from source) /** - * Live configuration reading from application.conf and initializing StringFormater for live + * Live configuration reading from application.conf and initializing StringFormater */ val live: ZLayer[Any, Nothing, AppConfig] = ZLayer { From 33dfffe66fc9fd347245784f2e48f62e422dd821 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Thu, 15 Sep 2022 17:06:48 +0200 Subject: [PATCH 26/44] improve readability of code and clean up --- .../org/knora/webapi/config/AppConfig.scala | 6 +- .../org/knora/webapi/core/ActorSystem.scala | 6 +- .../org/knora/webapi/core/AppRouter.scala | 2 +- .../org/knora/webapi/core/AppServer.scala | 114 ++++++++++-------- .../org/knora/webapi/core/LayersLive.scala | 8 +- .../webapi/messages/util/MessageUtil.scala | 3 +- .../store/iiif/impl/IIIFServiceSipiImpl.scala | 7 +- .../upgrade/RepositoryUpdater.scala | 45 ++++--- .../org/knora/webapi/core/LayersTest.scala | 48 ++++---- 9 files changed, 125 insertions(+), 114 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index a6a0aa8c4c..5c5818cef5 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -5,8 +5,11 @@ import zio._ import zio.config._ import java.nio.file.Files +import java.nio.file.Path import java.nio.file.Paths import scala.concurrent.duration +import scala.util.Failure +import scala.util.Success import scala.util.Try import dsp.errors.FileWriteException @@ -16,9 +19,6 @@ import org.knora.webapi.util.cache.CacheUtil import typesafe._ import magnolia._ -import scala.util.Success -import scala.util.Failure -import java.nio.file.Path /** * Represents the configuration as defined in application.conf. diff --git a/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala b/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala index df84c43a50..2648a28822 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala @@ -23,14 +23,14 @@ trait ActorSystem { object ActorSystem { - private def acquire(config: AppConfig, ec: ExecutionContext): URIO[Any, actor.ActorSystem] = + private def acquire(executionContext: ExecutionContext): URIO[Any, actor.ActorSystem] = ZIO .attempt( akka.actor.ActorSystem( name = "webapi", config = None, classLoader = None, - defaultExecutionContext = Some(ec) + defaultExecutionContext = Some(executionContext) ) ) .tap(_ => ZIO.logInfo(">>> Acquire Actor System <<<")) @@ -47,7 +47,7 @@ object ActorSystem { for { config <- ZIO.service[AppConfig] context <- ZIO.executor.map(_.asExecutionContext) - actorSystem <- ZIO.acquireRelease(acquire(config, context))(release _) + actorSystem <- ZIO.acquireRelease(acquire(context))(release _) } yield new ActorSystem { override val system: akka.actor.ActorSystem = actorSystem override val appConfig = config diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala index eb3350d1ce..b8e95d0c4c 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala @@ -45,7 +45,7 @@ object AppRouter { triplestoreServiceManager <- ZIO.service[TriplestoreServiceManager] appConfig <- ZIO.service[AppConfig] runtime <- ZIO.runtime[Any] - } yield new AppRouter { self => + } yield new AppRouter { implicit val system: akka.actor.ActorSystem = as.system implicit val executionContext: ExecutionContext = system.dispatcher diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index ec56345cc2..f4df077e3c 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -52,18 +52,20 @@ final case class AppServer( /** * Initiates repository upgrade if `requiresRepository` is `true` an logs the result. + * + * @param requiresRepository If `true`, calls the RepositoryUpdater to initiate the repository, otherwise returns () */ private def upgradeRepository(requiresRepository: Boolean): ZIO[Any, Nothing, Unit] = for { _ <- state.set(AppState.UpdatingRepository) - _ <- if (requiresRepository) - ru.maybeUpgradeRepository.flatMap(response => ZIO.logInfo(response.message)) - else - ZIO.unit + _ <- if (requiresRepository) ru.maybeUpgradeRepository.flatMap(response => ZIO.logInfo(response.message)) + else ZIO.unit _ <- state.set(AppState.RepositoryUpToDate) } yield () - /* Initiates building of all caches */ + /** + * Initiates building of all caches + */ private val buildAllCaches: ZIO[Any, Nothing, Unit] = for { _ <- state.set(AppState.CreatingCaches) @@ -74,18 +76,24 @@ final case class AppServer( _ <- state.set(AppState.CachesReady) } yield () - /* Initiates population of the ontology caches if `requiresRepository` is `true` */ + /** + * Initiates population of the ontology caches if `requiresRepository` is `true` + * + * @param requiresRepository If `true`, calls the AppRouter to populate the ontology caches, otherwise returns () + */ private def populateOntologyCaches(requiresRepository: Boolean): ZIO[Any, Nothing, Unit] = for { _ <- state.set(AppState.LoadingOntologies) - _ <- if (requiresRepository) - ar.populateOntologyCaches - else - ZIO.unit + _ <- if (requiresRepository) ar.populateOntologyCaches + else ZIO.unit _ <- state.set(AppState.OntologiesReady) } yield () - /* Checks if the IIIF service is running */ + /** + * Checks if the IIIF service is running + * + * @param requiresIIIFService If `true`, checks the status of the IIIFService instance, otherwise returns () + */ private def checkIIIFService(requiresIIIFService: Boolean): ZIO[Any, Nothing, Unit] = for { _ <- state.set(AppState.WaitingForIIIFService) @@ -105,7 +113,9 @@ final case class AppServer( _ <- state.set(AppState.IIIFServiceReady) } yield () - /* Checks if the Cache service is running */ + /** + * Checks if the Cache service is running + */ private val checkCacheService: ZIO[Any, Nothing, Unit] = for { _ <- state.set(AppState.WaitingForCacheService) @@ -122,26 +132,11 @@ final case class AppServer( } yield () /** - * Prints the welcome message - */ - private val printBanner: ZIO[Any, Nothing, Unit] = - for { - _ <- - ZIO.logInfo( - s"DSP-API Server started: ${appConfig.knoraApi.internalKnoraApiBaseUrl}" - ) - - _ = if (appConfig.allowReloadOverHttp) { - ZIO.logWarning("Resetting DB over HTTP is turned ON") - } - } yield () - - /** - * Initiates the startup sequence of the DSP-API server. + * Initiates the startup of the DSP-API server. * - * @param requiresRepository if `true`, check if it is running, run upgrading and loading ontology cache. - * If `false`, check if it is running but don't run upgrading AND loading ontology cache. - * @param requiresIIIFService if `true`, ensure that the IIIF service is started. + * @param requiresRepository If `true`, checks if repository service is running, updates data if necessary and loads ontology cache. + * If `false`, checks if repository service is running but doesn't run upgrades and doesn't load ontology cache. + * @param requiresIIIFService If `true`, ensures that the IIIF service is running. */ def start( requiresRepository: Boolean, @@ -156,7 +151,8 @@ final case class AppServer( _ <- checkIIIFService(requiresIIIFService) _ <- checkCacheService _ <- ZIO.logInfo("=> Startup checks finished") - _ <- printBanner + _ <- ZIO.logInfo(s"DSP-API Server started: ${appConfig.knoraApi.internalKnoraApiBaseUrl}") + _ = if (appConfig.allowReloadOverHttp) { ZIO.logWarning("Resetting DB over HTTP is turned ON") } _ <- state.set(AppState.Running) } yield () } @@ -164,7 +160,8 @@ final case class AppServer( object AppServer { private type AppServerEnvironment = - TriplestoreService + State + with TriplestoreService with RepositoryUpdater with ActorSystem with AppRouter @@ -172,29 +169,34 @@ object AppServer { with CacheService with HttpServer with AppConfig - with State - def startup( - requiresRepository: Boolean, - requiresIIIFService: Boolean - ): ZIO[AppServerEnvironment, Nothing, Unit] = + /** + * Initializes the AppServer instance with the required services + */ + def init( + ): ZIO[AppServerEnvironment, Nothing, AppServer] = for { - state <- ZIO.service[State] - ts <- ZIO.service[TriplestoreService] - ru <- ZIO.service[RepositoryUpdater] - as <- ZIO.service[ActorSystem] - ar <- ZIO.service[AppRouter] - iiifs <- ZIO.service[IIIFService] - cs <- ZIO.service[CacheService] - hs <- ZIO.service[HttpServer] - config <- ZIO.service[AppConfig] - _ <- AppServer(state, ts, ru, as, ar, iiifs, cs, hs, config).start(requiresRepository, requiresIIIFService) - } yield () + state <- ZIO.service[State] + ts <- ZIO.service[TriplestoreService] + ru <- ZIO.service[RepositoryUpdater] + as <- ZIO.service[ActorSystem] + ar <- ZIO.service[AppRouter] + iiifs <- ZIO.service[IIIFService] + cs <- ZIO.service[CacheService] + hs <- ZIO.service[HttpServer] + config <- ZIO.service[AppConfig] + appServer = AppServer(state, ts, ru, as, ar, iiifs, cs, hs, config) + } yield appServer - /* Live version */ + /** + * The AppServer live layer + */ val live: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { - startup(requiresRepository = true, requiresIIIFService = true) + for { + appServer <- AppServer.init() + _ <- appServer.start(requiresRepository = true, requiresIIIFService = true) + } yield () } /** @@ -203,7 +205,10 @@ object AppServer { */ val testWithSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { - startup(requiresRepository = false, requiresIIIFService = true) + for { + appServer <- AppServer.init() + _ <- appServer.start(requiresRepository = false, requiresIIIFService = true) + } yield () } /** @@ -212,6 +217,9 @@ object AppServer { */ val testWithoutSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { - startup(requiresRepository = false, requiresIIIFService = false) + for { + appServer <- AppServer.init() + _ <- appServer.start(requiresRepository = false, requiresIIIFService = false) + } yield () } } 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 415cdd2993..82cfabe441 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -37,11 +37,11 @@ object LayersLive { with TriplestoreServiceManager with TriplestoreService - // all effect layers needed to provide the `Environment` + /** + * All effect layers needed to provide the `Environment` + */ val dspLayersLive = - ZLayer.make[ - DspEnvironmentLive - ]( + ZLayer.make[DspEnvironmentLive]( ActorSystem.layer, ApiRoutes.layer, AppConfig.live, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/MessageUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/MessageUtil.scala index afd749b124..54b73834a0 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/MessageUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/MessageUtil.scala @@ -26,8 +26,7 @@ object MessageUtil { "stringFormatter", "base64Decoder", "knoraIdUtil", - "standoffLinkTagTargetResourceIris", - "knoraSettings" + "standoffLinkTagTargetResourceIris" ) /** diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala index d5ceb03d2e..ff16f9aaf8 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala @@ -31,7 +31,6 @@ import dsp.errors.BadRequestException import dsp.errors.NotFoundException import org.knora.webapi.auth.JWTService import org.knora.webapi.config.AppConfig -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.sipimessages._ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.store.iiif.api.IIIFService @@ -41,6 +40,10 @@ import org.knora.webapi.util.SipiUtil /** * Makes requests to Sipi. + * + * @param config The application's configuration + * @param jwt The JWT Service to handle JWT Tokens + * @param httpClient The HTTP Client */ case class IIIFServiceSipiImpl( config: AppConfig, @@ -48,8 +51,6 @@ case class IIIFServiceSipiImpl( httpClient: CloseableHttpClient ) extends IIIFService { - implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - /** * Asks Sipi for metadata about a file, served from the 'knora.json' route. * diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala index 33d0f88727..d974ee6653 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/upgrade/RepositoryUpdater.scala @@ -12,7 +12,6 @@ import scala.reflect.io.Directory import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi.IRI -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.store.triplestoremessages._ import org.knora.webapi.messages.util.rdf._ import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -23,7 +22,7 @@ import org.knora.webapi.util.FileUtil trait RepositoryUpdater { /** - * Upgrades the repository, if necessary, to work with the current version of dsp-api. + * Upgrades the repository, if necessary, to work with the current version of DSP-API. * * @return a response indicating what was done. */ @@ -33,18 +32,18 @@ trait RepositoryUpdater { /** * Updates a DSP repository to work with the current version of DSP-API. * - * Depends on [[TriplestoreService]] and [[AppConfig]]. + * Depends on [[TriplestoreService]]. */ object RepositoryUpdater { - val layer: ZLayer[TriplestoreService & AppConfig, Nothing, RepositoryUpdater] = + val layer: ZLayer[TriplestoreService, Nothing, RepositoryUpdater] = ZLayer { for { - ts <- ZIO.service[TriplestoreService] - config <- ZIO.service[AppConfig] + ts <- ZIO.service[TriplestoreService] } yield new RepositoryUpdaterImpl(ts) {} } - sealed abstract private class RepositoryUpdaterImpl(ts: TriplestoreService) extends RepositoryUpdater { + sealed abstract private class RepositoryUpdaterImpl(triplestoreService: TriplestoreService) + extends RepositoryUpdater { private val rdfFormatUtil: RdfFormatUtil = RdfFeatureFactory.getRdfFormatUtil() // A SPARQL query to find out the knora-base version in a repository. @@ -60,10 +59,10 @@ object RepositoryUpdater { */ private val log: Logger = Logger(LoggerFactory.getLogger(getClass.getName)) - private val tempDirNamePrefix: String = "knora" + private val tmpDirNamePrefix: String = "knora" /** - * Updates the repository, if necessary, to work with the current version of dsp-api. + * Updates the repository, if necessary, to work with the current version of DSP-API. * * @return a response indicating what was done. */ @@ -84,9 +83,9 @@ object RepositoryUpdater { // No. Construct the list of updates that it needs. _ <- ZIO.logInfo( - s"Repository not up-to-date. Found: ${foundRepositoryVersion.getOrElse("None")}, Required: $requiredRepositoryVersion" + s"Repository not up to date. Found: ${foundRepositoryVersion.getOrElse("None")}, Required: $requiredRepositoryVersion" ) - _ <- deleteTempDirectories() + _ <- deleteTmpDirectories() selectedPlugins <- selectPluginsForNeededUpdates(foundRepositoryVersion) _ <- ZIO.logInfo( @@ -100,18 +99,18 @@ object RepositoryUpdater { } yield repositoryUpdatedResponse /** - * Deletes directories inside temp directory starting with `tempDirNamePrefix`. + * Deletes directories inside tmp directory starting with `tmpDirNamePrefix`. */ - private def deleteTempDirectories(): UIO[Unit] = ZIO.attempt { - val rootDir = new File("/tmp/") - val getTempToDelete = rootDir.listFiles.filter(_.getName.startsWith(tempDirNamePrefix)) + private def deleteTmpDirectories(): UIO[Unit] = ZIO.attempt { + val rootDir = new File("/tmp/") + val getTmpToDelete = rootDir.listFiles.filter(_.getName.startsWith(tmpDirNamePrefix)) - if (getTempToDelete.length != 0) { - getTempToDelete.foreach { dir => + if (getTmpToDelete.length != 0) { + getTmpToDelete.foreach { dir => val dirToDelete = new Directory(dir) dirToDelete.deleteRecursively() } - log.info(s"Deleted temp directories: ${getTempToDelete.map(_.getName()).mkString(", ")}") + log.info(s"Deleted tmp directories: ${getTmpToDelete.map(_.getName()).mkString(", ")}") } () }.orDie @@ -123,7 +122,7 @@ object RepositoryUpdater { */ private def getRepositoryVersion(): UIO[Option[String]] = for { - repositoryVersionResponse <- ts.sparqlHttpSelect(knoraBaseVersionQuery) + repositoryVersionResponse <- triplestoreService.sparqlHttpSelect(knoraBaseVersionQuery) bindings <- ZIO.succeed(repositoryVersionResponse.results.bindings) versionString <- if (bindings.nonEmpty) { @@ -184,7 +183,7 @@ object RepositoryUpdater { pluginsForNeededUpdates: Seq[PluginForKnoraBaseVersion] ): UIO[RepositoryUpdatedResponse] = (for { - downloadDir <- ZIO.attempt(Files.createTempDirectory(tempDirNamePrefix)) + downloadDir <- ZIO.attempt(Files.createTempDirectory(tmpDirNamePrefix)) _ <- ZIO.logInfo(s"Repository update using download directory $downloadDir") // The file to save the repository in. @@ -193,7 +192,7 @@ object RepositoryUpdater { // Ask the store actor to download the repository to the file. _ <- ZIO.logInfo("Downloading repository file...") - _ <- ts.downloadRepository(downloadedRepositoryFile) + _ <- triplestoreService.downloadRepository(downloadedRepositoryFile) // Run the transformations to produce an output file. _ <- doTransformations( @@ -204,11 +203,11 @@ object RepositoryUpdater { // Empty the repository. _ <- ZIO.logInfo("Emptying the repository...") - _ <- ts.dropAllTriplestoreContent() + _ <- triplestoreService.dropAllTriplestoreContent() // Upload the transformed repository. _ <- ZIO.logInfo("Uploading transformed repository data...") - _ <- ts.uploadRepository(transformedRepositoryFile) + _ <- triplestoreService.uploadRepository(transformedRepositoryFile) } yield RepositoryUpdatedResponse( message = s"Updated repository to ${org.knora.webapi.KnoraBaseVersion}" )).orDie diff --git a/webapi/src/test/scala/org/knora/webapi/core/LayersTest.scala b/webapi/src/test/scala/org/knora/webapi/core/LayersTest.scala index 3a2c085896..3e98425ef1 100644 --- a/webapi/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/webapi/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -19,14 +19,17 @@ import org.knora.webapi.testservices.TestClientService object LayersTest { + /** + * The `Environment`s that we require for the tests to run - with or without Sipi + */ type DefaultTestEnvironmentWithoutSipi = LayersLive.DspEnvironmentLive with FusekiTestContainer with TestClientService type DefaultTestEnvironmentWithSipi = DefaultTestEnvironmentWithoutSipi with SipiTestContainer - // All live layers and both Fuseki and Sipi testcontainers + /** + * All live layers with both Fuseki and Sipi testcontainers + */ val defaultLayersTestWithSipi = - ZLayer.make[ - DefaultTestEnvironmentWithSipi - ]( + ZLayer.make[DefaultTestEnvironmentWithSipi]( ActorSystem.layer, ApiRoutes.layer, AppConfigForTestContainers.testcontainers, @@ -48,11 +51,11 @@ object LayersTest { TestClientService.layer ) - // All live layers but without sipi testcontainer + /** + * All live layers - with ActorSystem - but without Sipi testcontainer + */ val defaultLayersTestWithoutSipi = - ZLayer.make[ - DefaultTestEnvironmentWithoutSipi - ]( + ZLayer.make[DefaultTestEnvironmentWithoutSipi]( ActorSystem.layer, ApiRoutes.layer, AppConfigForTestContainers.fusekiOnlyTestcontainer, @@ -72,11 +75,12 @@ object LayersTest { // Test services TestClientService.layer ) - // All live layers but without sipi testcontainer + + /** + * All live layers - with ActorSystemTest - but without Sipi testcontainer + */ def defaultLayersTestWithoutSipi(system: akka.actor.ActorSystem) = - ZLayer.make[ - DefaultTestEnvironmentWithoutSipi - ]( + ZLayer.make[DefaultTestEnvironmentWithoutSipi]( ActorSystemTest.layer(system), ApiRoutes.layer, AppConfigForTestContainers.fusekiOnlyTestcontainer, @@ -97,11 +101,11 @@ object LayersTest { TestClientService.layer ) - // All live layers but with the mocked IIIF layer + /** + * All live layers - with ActorSystemTest - but with the mocked IIIF layer + */ val defaultLayersTestWithMockedSipi = - ZLayer.make[ - DefaultTestEnvironmentWithoutSipi - ]( + ZLayer.make[DefaultTestEnvironmentWithoutSipi]( ActorSystem.layer, ApiRoutes.layer, AppConfigForTestContainers.fusekiOnlyTestcontainer, @@ -110,7 +114,7 @@ object LayersTest { CacheServiceInMemImpl.layer, HttpServer.layer, IIIFServiceManager.layer, - IIIFServiceMockImpl.layer, // alternative: IIIFServiceMockImpl.layer + IIIFServiceMockImpl.layer, JWTService.layer, RepositoryUpdater.layer, State.layer, @@ -122,11 +126,11 @@ object LayersTest { TestClientService.layer ) - // All live layers but with the mocked IIIF layer + /** + * All live layers - with ActorSystemTest - but with the mocked IIIF layer + */ def defaultLayersTestWithMockedSipi(system: akka.actor.ActorSystem) = - ZLayer.make[ - DefaultTestEnvironmentWithoutSipi - ]( + ZLayer.make[DefaultTestEnvironmentWithoutSipi]( ActorSystemTest.layer(system), ApiRoutes.layer, AppConfigForTestContainers.fusekiOnlyTestcontainer, @@ -135,7 +139,7 @@ object LayersTest { CacheServiceInMemImpl.layer, HttpServer.layer, IIIFServiceManager.layer, - IIIFServiceMockImpl.layer, // alternative: IIIFServiceMockImpl.layer + IIIFServiceMockImpl.layer, JWTService.layer, RepositoryUpdater.layer, State.layer, From 9c66344d242ad05dc605e52c59701beb24153ec2 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 08:13:59 +0200 Subject: [PATCH 27/44] Create .codecov.yml --- .codecov.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000000..bfdc9877d9 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true From b2a94abadfb644e4b3dbf24ce9198410d76d3d71 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 11:19:26 +0200 Subject: [PATCH 28/44] simplify and clean-up --- .../org/knora/webapi/auth/JWTService.scala | 12 ++++--- .../org/knora/webapi/config/AppConfig.scala | 25 ++++--------- .../org/knora/webapi/core/AppRouter.scala | 3 +- .../org/knora/webapi/core/AppServer.scala | 36 +++++++++++-------- .../org/knora/webapi/core/HttpServer.scala | 10 +++--- .../org/knora/webapi/core/LayersLive.scala | 2 +- .../webapi/core/actors/RoutingActor.scala | 10 ++---- .../responders/v2/ResourcesResponderV2.scala | 4 --- .../responders/v2/StandoffResponderV2.scala | 4 --- .../webapi/routing/v2/SearchRouteV2.scala | 3 +- .../store/cache/CacheServiceManager.scala | 20 +++++------ .../store/iiif/impl/IIIFServiceSipiImpl.scala | 14 +++----- .../knora/webapi/config/AppConfigZSpec.scala | 2 +- .../knora/webapi/e2e/v1/ErrorV1E2ESpec.scala | 2 -- .../v2/ResourcesResponderV2Spec.scala | 3 -- .../org/knora/webapi/util/AkkaHttpUtils.scala | 9 ++--- 16 files changed, 63 insertions(+), 96 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/auth/JWTService.scala b/webapi/src/main/scala/org/knora/webapi/auth/JWTService.scala index 5a60b0b962..a28ac84bfb 100644 --- a/webapi/src/main/scala/org/knora/webapi/auth/JWTService.scala +++ b/webapi/src/main/scala/org/knora/webapi/auth/JWTService.scala @@ -3,11 +3,13 @@ package org.knora.webapi.auth import spray.json.JsValue import zio._ +import scala.concurrent.duration + import org.knora.webapi._ import org.knora.webapi.config._ import org.knora.webapi.routing.JWTHelper -final case class JWTService(config: AppConfig) { +final case class JWTService(secret: String, longevity: duration.Duration, issuer: String) { /** * Creates a new JWT token for a specific user and holds some additional @@ -20,9 +22,9 @@ final case class JWTService(config: AppConfig) { ZIO.succeed { JWTHelper.createToken( userIri = id, - secret = config.jwtSecretKey, - longevity = config.jwtLongevityAsDuration, - issuer = config.knoraApi.externalKnoraApiHostPort, + secret = secret, + longevity = longevity, + issuer = issuer, content = content ) } @@ -33,6 +35,6 @@ object JWTService { ZLayer { for { config <- ZIO.service[AppConfig] - } yield JWTService(config) + } yield JWTService(config.jwtSecretKey, config.jwtLongevityAsDuration, config.knoraApi.externalKnoraApiHostPort) } } diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 5c5818cef5..a7eb50f036 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -13,8 +13,6 @@ import scala.util.Success import scala.util.Try import dsp.errors.FileWriteException -import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.util.rdf.RdfFeatureFactory import org.knora.webapi.util.cache.CacheUtil import typesafe._ @@ -249,29 +247,18 @@ object AppConfig { /** * Instantiates our config class hierarchy using the data from the 'app' configuration from 'application.conf'. */ - private val config: IO[ReadError[String], AppConfig] = read(descriptor[AppConfig].mapKey(toKebabCase) from source) + private val configFromSource: IO[ReadError[String], AppConfig] = read( + descriptor[AppConfig].mapKey(toKebabCase) from source + ) /** - * Live configuration reading from application.conf and initializing StringFormater + * Application configuration from application.conf */ - val live: ZLayer[Any, Nothing, AppConfig] = + val layer: ZLayer[Any, Nothing, AppConfig] = ZLayer { for { - c <- config.orDie - _ <- ZIO.attempt(StringFormatter.init(c)).orDie // needs early init before first usage - _ <- ZIO.attempt(RdfFeatureFactory.init(c)).orDie // needs early init before first usage + c <- configFromSource.orDie } yield c }.tap(_ => ZIO.logInfo(">>> AppConfig Live Initialized <<<")) - /** - * Test configuration reading from application.conf and initializing StringFormater for tests - */ - val test: ZLayer[Any, Nothing, AppConfig] = - ZLayer { - for { - c <- config.orDie - _ <- ZIO.attempt(StringFormatter.initForTest()).orDie // needs early init before first usage - _ <- ZIO.attempt(RdfFeatureFactory.init(c)).orDie // needs early init before first usage - } yield c - }.tap(_ => ZIO.logInfo(">>> AppConfig Test Initialized <<<")) } diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala index b8e95d0c4c..6994261922 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala @@ -46,8 +46,7 @@ object AppRouter { appConfig <- ZIO.service[AppConfig] runtime <- ZIO.runtime[Any] } yield new AppRouter { - implicit val system: akka.actor.ActorSystem = as.system - implicit val executionContext: ExecutionContext = system.dispatcher + implicit val system: akka.actor.ActorSystem = as.system val ref: ActorRef = system.actorOf( Props( diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index f4df077e3c..c6e28808b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -9,10 +9,12 @@ import zio._ import org.knora.webapi.config.AppConfig import org.knora.webapi.core.domain.AppState +import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusNOK import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusOK import org.knora.webapi.messages.store.sipimessages.IIIFServiceStatusNOK import org.knora.webapi.messages.store.sipimessages.IIIFServiceStatusOK +import org.knora.webapi.messages.util.rdf.RdfFeatureFactory import org.knora.webapi.store.cache.api.CacheService import org.knora.webapi.store.iiif.api.IIIFService import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -152,7 +154,7 @@ final case class AppServer( _ <- checkCacheService _ <- ZIO.logInfo("=> Startup checks finished") _ <- ZIO.logInfo(s"DSP-API Server started: ${appConfig.knoraApi.internalKnoraApiBaseUrl}") - _ = if (appConfig.allowReloadOverHttp) { ZIO.logWarning("Resetting DB over HTTP is turned ON") } + _ = if (appConfig.allowReloadOverHttp) ZIO.logWarning("Resetting DB over HTTP is turned ON") _ <- state.set(AppState.Running) } yield () } @@ -172,20 +174,24 @@ object AppServer { /** * Initializes the AppServer instance with the required services + * + * @param test If `true`, initiates the string formatter as test */ - def init( - ): ZIO[AppServerEnvironment, Nothing, AppServer] = + def init(test: Boolean = false): ZIO[AppServerEnvironment, Nothing, AppServer] = for { - state <- ZIO.service[State] - ts <- ZIO.service[TriplestoreService] - ru <- ZIO.service[RepositoryUpdater] - as <- ZIO.service[ActorSystem] - ar <- ZIO.service[AppRouter] - iiifs <- ZIO.service[IIIFService] - cs <- ZIO.service[CacheService] - hs <- ZIO.service[HttpServer] - config <- ZIO.service[AppConfig] - appServer = AppServer(state, ts, ru, as, ar, iiifs, cs, hs, config) + state <- ZIO.service[State] + ts <- ZIO.service[TriplestoreService] + ru <- ZIO.service[RepositoryUpdater] + as <- ZIO.service[ActorSystem] + ar <- ZIO.service[AppRouter] + iiifs <- ZIO.service[IIIFService] + cs <- ZIO.service[CacheService] + hs <- ZIO.service[HttpServer] + c <- ZIO.service[AppConfig] + _ <- ZIO.attempt(RdfFeatureFactory.init(c)).orDie // needs early init before first usage + _ = if (test) ZIO.attempt(StringFormatter.initForTest()).orDie // needs early init before first usage + else ZIO.attempt(StringFormatter.init(c)).orDie + appServer = AppServer(state, ts, ru, as, ar, iiifs, cs, hs, c) } yield appServer /** @@ -206,7 +212,7 @@ object AppServer { val testWithSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { for { - appServer <- AppServer.init() + appServer <- AppServer.init(test = true) _ <- appServer.start(requiresRepository = false, requiresIIIFService = true) } yield () } @@ -218,7 +224,7 @@ object AppServer { val testWithoutSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { for { - appServer <- AppServer.init() + appServer <- AppServer.init(test = true) _ <- appServer.start(requiresRepository = false, requiresIIIFService = false) } yield () } diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index 483a0481f6..9d3a427279 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -6,15 +6,15 @@ package org.knora.webapi.core import akka.http.scaladsl.Http -import akka.stream.Materializer import zio._ -import scala.concurrent.ExecutionContext - import org.knora.webapi.config.AppConfig import org.knora.webapi.core import org.knora.webapi.routing.ApiRoutes +/** + * The Akka based HTTP server + */ trait HttpServer { val serverBinding: Http.ServerBinding } @@ -27,9 +27,7 @@ object HttpServer { config <- ZIO.service[AppConfig] apiRoutes <- ZIO.service[ApiRoutes] binding <- { - implicit val system: akka.actor.ActorSystem = as.system - implicit val materializer: Materializer = Materializer.matFromSystem(system) - implicit val executionContext: ExecutionContext = system.dispatcher + implicit val system: akka.actor.ActorSystem = as.system ZIO.acquireRelease { ZIO 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 82cfabe441..ad44635a6f 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -44,7 +44,7 @@ object LayersLive { ZLayer.make[DspEnvironmentLive]( ActorSystem.layer, ApiRoutes.layer, - AppConfig.live, + AppConfig.layer, AppRouter.layer, CacheServiceManager.layer, CacheServiceInMemImpl.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 8e6b445015..b91a20f966 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 @@ -89,11 +89,6 @@ class RoutingActor( */ implicit val executionContext: ExecutionContext = context.dispatcher - /** - * Timeout definition - */ - implicit protected val timeout: Timeout = appConfig.defaultTimeoutAsDuration - /** * Data used in responders. */ @@ -129,6 +124,7 @@ class RoutingActor( def receive: Receive = { + // V1 request messages case ckanResponderRequestV1: CkanResponderRequestV1 => ActorUtil.future2Message(sender(), ckanResponderV1.receive(ckanResponderRequestV1), log) case resourcesResponderRequestV1: ResourcesResponderRequestV1 => @@ -148,7 +144,7 @@ class RoutingActor( case projectsResponderRequestV1: ProjectsResponderRequestV1 => ActorUtil.future2Message(sender(), projectsResponderV1.receive(projectsResponderRequestV1), log) - // Knora API V2 messages + // V2 request messages case ontologiesResponderRequestV2: OntologiesResponderRequestV2 => ActorUtil.future2Message(sender(), ontologiesResponderV2.receive(ontologiesResponderRequestV2), log) case searchResponderRequestV2: SearchResponderRequestV2 => @@ -162,7 +158,7 @@ class RoutingActor( case listsResponderRequestV2: ListsResponderRequestV2 => ActorUtil.future2Message(sender(), listsResponderV2.receive(listsResponderRequestV2), log) - // Knora Admin message + // Admin request messages case groupsResponderRequestADM: GroupsResponderRequestADM => ActorUtil.future2Message(sender(), groupsResponderADM.receive(groupsResponderRequestADM), log) case listsResponderRequest: ListsResponderRequestADM => 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 58fc8161e7..47b6926d71 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 @@ -7,7 +7,6 @@ package org.knora.webapi.responders.v2 import akka.http.scaladsl.util.FastFuture import akka.pattern._ -import akka.stream.Materializer import java.time.Instant import java.util.UUID @@ -68,9 +67,6 @@ import org.knora.webapi.util._ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) extends ResponderWithStandoffV2(responderData, appConfig) { - /* actor materializer needed for http requests */ - implicit val materializer: Materializer = Materializer.matFromSystem(system) - /** * Represents a resource that is ready to be created and whose contents can be verified afterwards. * 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 beaae477ad..e934b00135 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 @@ -6,7 +6,6 @@ package org.knora.webapi.responders.v2 import akka.pattern._ -import akka.stream.Materializer import akka.util.Timeout import org.xml.sax.SAXException @@ -66,9 +65,6 @@ import org.knora.webapi.util.cache.CacheUtil class StandoffResponderV2(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData, appConfig) { - /* actor materializer needed for http requests */ - implicit val materializer: Materializer = Materializer.matFromSystem(system) - private def xmlMimeTypes = Set( "text/xml", "application/xml" diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala index 18c48c2aae..366938ce72 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala @@ -7,6 +7,7 @@ package org.knora.webapi.routing.v2 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import zio.ZIO import scala.concurrent.Future @@ -210,7 +211,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) private def fullTextSearch(): Route = path("v2" / "search" / Segment) { searchStr => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => - log.info(s"Full Text Search for string: $searchStr") + ZIO.logInfo(s"Full-text Search for string: $searchStr") if (searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname)) { throw BadRequestException("It looks like you are submitting a Gravsearch request to a full-text search route") diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceManager.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceManager.scala index 92c46b189b..8ba1f1a251 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/CacheServiceManager.scala @@ -27,7 +27,7 @@ object CacheServiceManager { val layer: ZLayer[CacheService, Nothing, CacheServiceManager] = ZLayer { for { - cs <- ZIO.service[CacheService] + cacheService <- ZIO.service[CacheService] } yield new CacheServiceManager { val cacheServiceWriteUserTimer = Metric @@ -73,7 +73,7 @@ object CacheServiceManager { */ def putUserADM(value: UserADM): Task[Unit] = for { - res <- cs.putUserADM(value) @@ cacheServiceWriteUserTimer.trackDuration + res <- cacheService.putUserADM(value) @@ cacheServiceWriteUserTimer.trackDuration // _ <- cacheServiceWriteUserTimer.value.tap(value => ZIO.debug(value)) } yield res @@ -84,7 +84,7 @@ object CacheServiceManager { * @param id the project identifier. */ def getUserADM(id: UserIdentifierADM): Task[Option[UserADM]] = - cs.getUserADM(id) + cacheService.getUserADM(id) /** * Stores the project under the IRI and additionally the IRI under the keys @@ -98,7 +98,7 @@ object CacheServiceManager { */ def putProjectADM(value: ProjectADM): Task[Unit] = for { - res <- cs.putProjectADM(value) @@ cacheServiceWriteProjectTimer.trackDuration + res <- cacheService.putProjectADM(value) @@ cacheServiceWriteProjectTimer.trackDuration // _ <- cacheServiceWriteProjectTimer.value.tap(value => ZIO.debug(value)) } yield res @@ -109,7 +109,7 @@ object CacheServiceManager { */ def getProjectADM(id: ProjectIdentifierADM): Task[Option[ProjectADM]] = for { - res <- cs.getProjectADM(id) @@ cacheServiceReadProjectTimer.trackDuration + res <- cacheService.getProjectADM(id) @@ cacheServiceReadProjectTimer.trackDuration // _ <- cacheServiceReadProjectTimer.value.tap(value => ZIO.debug(value)) } yield res @@ -119,7 +119,7 @@ object CacheServiceManager { * @param k the key. */ def getStringValue(k: String): Task[Option[String]] = - cs.getStringValue(k) + cacheService.getStringValue(k) /** * Store string or byte array value under key. @@ -128,7 +128,7 @@ object CacheServiceManager { * @param v the value. */ def writeStringValue(k: String, v: String): Task[Unit] = - cs.putStringValue(k, v) + cacheService.putStringValue(k, v) /** * Removes values for the provided keys. Any invalid keys are ignored. @@ -136,19 +136,19 @@ object CacheServiceManager { * @param keys the keys. */ def removeValues(keys: Set[String]): Task[Unit] = - cs.removeValues(keys) + cacheService.removeValues(keys) /** * Flushes (removes) all stored content from the store. */ def flushDB(requestingUser: UserADM): Task[Unit] = - cs.flushDB(requestingUser) + cacheService.flushDB(requestingUser) /** * Pings the cache service to see if it is available. */ def ping(): UIO[CacheServiceStatusResponse] = - cs.getStatus + cacheService.getStatus } } } diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala index ff16f9aaf8..a8b9ea05b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala @@ -280,7 +280,7 @@ object IIIFServiceSipiImpl { * Acquires a configured httpClient, backed by a connection pool, * to be used in communicating with SIPI. */ - private def acquire(config: AppConfig) = ZIO.attemptBlocking { + private def acquire(config: AppConfig): URIO[Any, CloseableHttpClient] = ZIO.attemptBlocking { // timeout from config val sipiTimeoutMillis: Int = config.sipi.timeoutInSeconds.toMillis.toInt @@ -313,14 +313,12 @@ object IIIFServiceSipiImpl { .setSocketTimeout(sipiTimeoutMillis) .build() - // Create an HttpClient with the given custom dependencies and configuration. - val httpClient: CloseableHttpClient = HttpClients + // Return an HttpClient with the given custom dependencies and configuration. + HttpClients .custom() .setConnectionManager(connManager) .setDefaultRequestConfig(defaultRequestConfig) .build() - - httpClient }.tap(_ => ZIO.logInfo(">>> Acquire Sipi IIIF Service <<<")).orDie /** @@ -334,12 +332,10 @@ object IIIFServiceSipiImpl { val layer: ZLayer[AppConfig & JWTService, Nothing, IIIFService] = ZLayer.scoped { for { - config <- ZIO.service[AppConfig] - // _ <- ZIO.debug(config) + config <- ZIO.service[AppConfig] jwtService <- ZIO.service[JWTService] // HINT: Scope does not work when used together with unsafeRun to - // bridge over to Akka. Need to change this as soon Akka is removed - // httpClient <- ZIO.acquireRelease(acquire(config))(release(_)) + // bridge over to Akka. TODO Need to change this as soon as Akka is removed httpClient <- ZIO.acquireRelease(acquire(config))(release(_)) } yield IIIFServiceSipiImpl(config, jwtService, httpClient) } diff --git a/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala b/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala index 1fdaf3b81f..451bc78aac 100644 --- a/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala @@ -20,6 +20,6 @@ object AppConfigZSpec extends ZIOSpecDefault { assertTrue(appConfig.sipi.timeoutInSeconds == FiniteDuration(120L, TimeUnit.SECONDS)) && assertTrue(appConfig.bcryptPasswordStrength == User.PasswordStrength(12)) } - }.provideLayer(AppConfig.live) + }.provideLayer(AppConfig.layer) ) } diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ErrorV1E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ErrorV1E2ESpec.scala index 23b4ad7a05..c25632b22c 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ErrorV1E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ErrorV1E2ESpec.scala @@ -14,14 +14,12 @@ class ErrorV1E2ESpec extends E2ESpec with TriplestoreJsonProtocol { "Make a request that causes an internal server error (unit type message)" in { val request = Get(baseApiUrl + "/v1/error/unitMsg") val response = singleAwaitingRequest(request, 1.second) - println(response.toString()) assert(response.status == StatusCodes.InternalServerError) } "Make a request that causes an internal server error (ise type message)" in { val request = Get(baseApiUrl + "/v1/error/iseMsg") val response = singleAwaitingRequest(request, 1.second) - println(response.toString()) assert(response.status == StatusCodes.Conflict) } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala index 41f0da895f..3c5293806c 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala @@ -1902,7 +1902,6 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { resource.deletionInfo should not be (None) resource.lastModificationDate should not be (None) resource.creationDate should equal(aThingCreationDate) - println(resource) } } @@ -2210,7 +2209,6 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { appActor ! eraseRequest expectMsgPF(timeout) { case msg: akka.actor.Status.Failure => - // println(msg.cause) msg.cause.isInstanceOf[ForbiddenException] should ===(true) } } @@ -2266,7 +2264,6 @@ class ResourcesResponderV2Spec extends CoreSpec() with ImplicitSender { appActor ! eraseRequest expectMsgPF(timeout) { case msg: akka.actor.Status.Failure => - // println(msg.cause) msg.cause.isInstanceOf[BadRequestException] should ===(true) } diff --git a/webapi/src/test/scala/org/knora/webapi/util/AkkaHttpUtils.scala b/webapi/src/test/scala/org/knora/webapi/util/AkkaHttpUtils.scala index 0907f08d8c..be3720a490 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/AkkaHttpUtils.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/AkkaHttpUtils.scala @@ -9,7 +9,6 @@ import akka.actor.ActorSystem import akka.http.scaladsl.model.HttpResponse import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.Materializer import akka.util.Timeout import com.typesafe.scalalogging.LazyLogging import spray.json._ @@ -35,13 +34,9 @@ object AkkaHttpUtils extends LazyLogging { import DefaultJsonProtocol._ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ - implicit val materializer: Materializer = Materializer.matFromSystem(system) - val jsonFuture: Future[JsObject] = response match { - case HttpResponse(StatusCodes.OK, _, entity, _) => - Unmarshal(entity).to[JsObject] - case other => - throw new Exception(other.toString()) + case HttpResponse(StatusCodes.OK, _, entity, _) => Unmarshal(entity).to[JsObject] + case other => throw new Exception(other.toString()) } // FIXME: There is probably a better non blocking way of doing it. From b27d8b24fd876f44108249bc46c19d8b31e9a3a5 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 11:55:20 +0200 Subject: [PATCH 29/44] fix failing test --- .../org/knora/webapi/config/AppConfig.scala | 18 ++++++++++++++- .../org/knora/webapi/core/AppRouter.scala | 2 -- .../org/knora/webapi/core/AppServer.scala | 23 ++++++++----------- .../org/knora/webapi/core/LayersLive.scala | 2 +- .../webapi/core/actors/RoutingActor.scala | 1 - .../knora/webapi/config/AppConfigZSpec.scala | 2 +- 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index a7eb50f036..b05f8f24ef 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -13,6 +13,8 @@ import scala.util.Success import scala.util.Try import dsp.errors.FileWriteException +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.util.rdf.RdfFeatureFactory import org.knora.webapi.util.cache.CacheUtil import typesafe._ @@ -254,11 +256,25 @@ object AppConfig { /** * Application configuration from application.conf */ - val layer: ZLayer[Any, Nothing, AppConfig] = + val live: ZLayer[Any, Nothing, AppConfig] = ZLayer { for { c <- configFromSource.orDie + _ <- ZIO.attempt(RdfFeatureFactory.init(c)).orDie // needs early init before first usage + _ <- ZIO.attempt(StringFormatter.init(c)).orDie // needs early init before first usage } yield c }.tap(_ => ZIO.logInfo(">>> AppConfig Live Initialized <<<")) + /** + * Application configuration from test.conf for testing + */ + val test: ZLayer[Any, Nothing, AppConfig] = + ZLayer { + for { + c <- configFromSource.orDie + _ <- ZIO.attempt(RdfFeatureFactory.init(c)).orDie // needs early init before first usage + _ <- ZIO.attempt(StringFormatter.initForTest()).orDie // needs early init before first usage + } yield c + }.tap(_ => ZIO.logInfo(">>> AppConfig Test Initialized <<<")) + } diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala index 6994261922..41a9a52885 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppRouter.scala @@ -12,8 +12,6 @@ import akka.util.Timeout import zio._ import zio.macros.accessible -import scala.concurrent.ExecutionContext - import org.knora.webapi.config.AppConfig import org.knora.webapi.core import org.knora.webapi.messages.util.KnoraSystemInstances diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index c6e28808b4..9c148ceec3 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -9,12 +9,10 @@ import zio._ import org.knora.webapi.config.AppConfig import org.knora.webapi.core.domain.AppState -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusNOK import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusOK import org.knora.webapi.messages.store.sipimessages.IIIFServiceStatusNOK import org.knora.webapi.messages.store.sipimessages.IIIFServiceStatusOK -import org.knora.webapi.messages.util.rdf.RdfFeatureFactory import org.knora.webapi.store.cache.api.CacheService import org.knora.webapi.store.iiif.api.IIIFService import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -179,18 +177,15 @@ object AppServer { */ def init(test: Boolean = false): ZIO[AppServerEnvironment, Nothing, AppServer] = for { - state <- ZIO.service[State] - ts <- ZIO.service[TriplestoreService] - ru <- ZIO.service[RepositoryUpdater] - as <- ZIO.service[ActorSystem] - ar <- ZIO.service[AppRouter] - iiifs <- ZIO.service[IIIFService] - cs <- ZIO.service[CacheService] - hs <- ZIO.service[HttpServer] - c <- ZIO.service[AppConfig] - _ <- ZIO.attempt(RdfFeatureFactory.init(c)).orDie // needs early init before first usage - _ = if (test) ZIO.attempt(StringFormatter.initForTest()).orDie // needs early init before first usage - else ZIO.attempt(StringFormatter.init(c)).orDie + state <- ZIO.service[State] + ts <- ZIO.service[TriplestoreService] + ru <- ZIO.service[RepositoryUpdater] + as <- ZIO.service[ActorSystem] + ar <- ZIO.service[AppRouter] + iiifs <- ZIO.service[IIIFService] + cs <- ZIO.service[CacheService] + hs <- ZIO.service[HttpServer] + c <- ZIO.service[AppConfig] appServer = AppServer(state, ts, ru, as, ar, iiifs, cs, hs, c) } yield appServer 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 ad44635a6f..82cfabe441 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -44,7 +44,7 @@ object LayersLive { ZLayer.make[DspEnvironmentLive]( ActorSystem.layer, ApiRoutes.layer, - AppConfig.layer, + AppConfig.live, AppRouter.layer, CacheServiceManager.layer, CacheServiceInMemImpl.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 b91a20f966..0a5616e8f4 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 @@ -7,7 +7,6 @@ package org.knora.webapi.core.actors import akka.actor.Actor import akka.actor.ActorSystem -import akka.util.Timeout import com.typesafe.scalalogging.Logger import scala.concurrent.ExecutionContext diff --git a/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala b/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala index 451bc78aac..1fdaf3b81f 100644 --- a/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/config/AppConfigZSpec.scala @@ -20,6 +20,6 @@ object AppConfigZSpec extends ZIOSpecDefault { assertTrue(appConfig.sipi.timeoutInSeconds == FiniteDuration(120L, TimeUnit.SECONDS)) && assertTrue(appConfig.bcryptPasswordStrength == User.PasswordStrength(12)) } - }.provideLayer(AppConfig.layer) + }.provideLayer(AppConfig.live) ) } From 60298f7f5b8c0645324eef84d019d10e766bf320 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 13:51:44 +0200 Subject: [PATCH 30/44] remove appConfig as param for routes --- .../org/knora/webapi/routing/ApiRoutes.scala | 54 ++++---- .../knora/webapi/routing/HealthRoute.scala | 7 +- .../org/knora/webapi/routing/KnoraRoute.scala | 13 +- .../knora/webapi/routing/RejectingRoute.scala | 5 +- .../webapi/routing/admin/FilesRouteADM.scala | 7 +- .../webapi/routing/admin/GroupsRouteADM.scala | 19 ++- .../webapi/routing/admin/ListsRouteADM.scala | 11 +- .../routing/admin/PermissionsRouteADM.scala | 11 +- .../routing/admin/ProjectsRouteADM.scala | 43 +++---- .../webapi/routing/admin/StoreRouteADM.scala | 5 +- .../webapi/routing/admin/UsersRouteADM.scala | 43 +++---- .../admin/lists/CreateListItemsRouteADM.scala | 9 +- .../admin/lists/DeleteListItemsRouteADM.scala | 11 +- .../admin/lists/GetListItemsRouteADM.scala | 13 +- .../admin/lists/UpdateListItemsRouteADM.scala | 15 +-- .../CreatePermissionRouteADM.scala | 9 +- .../DeletePermissionRouteADM.scala | 7 +- .../permissions/GetPermissionsRouteADM.scala | 13 +- .../UpdatePermissionRouteADM.scala | 13 +- .../webapi/routing/v1/AssetsRouteV1.scala | 5 +- .../routing/v1/AuthenticationRouteV1.scala | 17 +-- .../knora/webapi/routing/v1/CkanRouteV1.scala | 7 +- .../webapi/routing/v1/ListsRouteV1.scala | 9 +- .../webapi/routing/v1/ProjectsRouteV1.scala | 11 +- .../routing/v1/ResourceTypesRouteV1.scala | 17 +-- .../webapi/routing/v1/ResourcesRouteV1.scala | 27 ++-- .../webapi/routing/v1/SearchRouteV1.scala | 9 +- .../webapi/routing/v1/StandoffRouteV1.scala | 7 +- .../webapi/routing/v1/UsersRouteV1.scala | 17 +-- .../webapi/routing/v1/ValuesRouteV1.scala | 21 ++- .../routing/v2/AuthenticationRouteV2.scala | 15 +-- .../webapi/routing/v2/ListsRouteV2.scala | 13 +- .../webapi/routing/v2/OntologiesRouteV2.scala | 121 +++++++++--------- .../webapi/routing/v2/ResourcesRouteV2.scala | 81 ++++++------ .../webapi/routing/v2/SearchRouteV2.scala | 53 ++++---- .../webapi/routing/v2/StandoffRouteV2.scala | 13 +- .../webapi/routing/v2/ValuesRouteV2.scala | 21 ++- .../webapi/e2e/v1/ResourcesV1R2RSpec.scala | 6 +- .../knora/webapi/e2e/v1/SearchV1R2RSpec.scala | 2 +- .../knora/webapi/e2e/v1/SipiV1R2RSpec.scala | 4 +- .../webapi/e2e/v1/StandoffV1R2RSpec.scala | 4 +- .../knora/webapi/e2e/v1/ValuesV1R2RSpec.scala | 2 +- .../e2e/v2/JSONLDHandlingV2R2RSpec.scala | 2 +- .../webapi/e2e/v2/ListsRouteV2R2RSpec.scala | 2 +- .../webapi/e2e/v2/OntologyV2R2RSpec.scala | 4 +- .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 2 +- .../webapi/e2e/v2/SearchRouteV2R2RSpec.scala | 8 +- .../knora/webapi/e2e/v2/ValuesV2R2RSpec.scala | 4 +- 48 files changed, 372 insertions(+), 440 deletions(-) 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 51d06789c0..9232b704ae 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -92,34 +92,34 @@ private final case class ApiRoutesImpl(routeData: KnoraRouteData, runtime: Runti DSPApiDirectives.handleErrors(routeData.system, appConfig) { CorsDirectives.cors(CorsSettings(routeData.system)) { DSPApiDirectives.handleErrors(routeData.system, appConfig) { - new HealthRoute(routeData, runtime, appConfig).makeRoute ~ + new HealthRoute(routeData, runtime).makeRoute ~ new VersionRoute().makeRoute ~ - new RejectingRoute(routeData.system, runtime, appConfig).makeRoute ~ - new ResourcesRouteV1(routeData, appConfig).makeRoute ~ - new ValuesRouteV1(routeData, appConfig).makeRoute ~ - new StandoffRouteV1(routeData, appConfig).makeRoute ~ - new ListsRouteV1(routeData, appConfig).makeRoute ~ - new ResourceTypesRouteV1(routeData, appConfig).makeRoute ~ - new SearchRouteV1(routeData, appConfig).makeRoute ~ - new AuthenticationRouteV1(routeData, appConfig).makeRoute ~ - new AssetsRouteV1(routeData, appConfig).makeRoute ~ - new CkanRouteV1(routeData, appConfig).makeRoute ~ - new UsersRouteV1(routeData, appConfig).makeRoute ~ - new ProjectsRouteV1(routeData, appConfig).makeRoute ~ - new OntologiesRouteV2(routeData, appConfig).makeRoute ~ - new SearchRouteV2(routeData, appConfig).makeRoute ~ - new ResourcesRouteV2(routeData, appConfig).makeRoute ~ - new ValuesRouteV2(routeData, appConfig).makeRoute ~ - new StandoffRouteV2(routeData, appConfig).makeRoute ~ - new ListsRouteV2(routeData, appConfig).makeRoute ~ - new AuthenticationRouteV2(routeData, appConfig).makeRoute ~ - new GroupsRouteADM(routeData, appConfig).makeRoute ~ - new ListsRouteADM(routeData, appConfig).makeRoute ~ - new PermissionsRouteADM(routeData, appConfig).makeRoute ~ - new ProjectsRouteADM(routeData, appConfig).makeRoute ~ - new StoreRouteADM(routeData, appConfig).makeRoute ~ - new UsersRouteADM(routeData, appConfig).makeRoute ~ - new FilesRouteADM(routeData, appConfig).makeRoute + new RejectingRoute(routeData, runtime).makeRoute ~ + new ResourcesRouteV1(routeData).makeRoute ~ + new ValuesRouteV1(routeData).makeRoute ~ + new StandoffRouteV1(routeData).makeRoute ~ + new ListsRouteV1(routeData).makeRoute ~ + new ResourceTypesRouteV1(routeData).makeRoute ~ + new SearchRouteV1(routeData).makeRoute ~ + new AuthenticationRouteV1(routeData).makeRoute ~ + new AssetsRouteV1(routeData).makeRoute ~ + new CkanRouteV1(routeData).makeRoute ~ + new UsersRouteV1(routeData).makeRoute ~ + new ProjectsRouteV1(routeData).makeRoute ~ + new OntologiesRouteV2(routeData).makeRoute ~ + new SearchRouteV2(routeData).makeRoute ~ + new ResourcesRouteV2(routeData).makeRoute ~ + new ValuesRouteV2(routeData).makeRoute ~ + new StandoffRouteV2(routeData).makeRoute ~ + new ListsRouteV2(routeData).makeRoute ~ + new AuthenticationRouteV2(routeData).makeRoute ~ + new GroupsRouteADM(routeData).makeRoute ~ + new ListsRouteADM(routeData).makeRoute ~ + new PermissionsRouteADM(routeData).makeRoute ~ + new ProjectsRouteADM(routeData).makeRoute ~ + new StoreRouteADM(routeData).makeRoute ~ + new UsersRouteADM(routeData).makeRoute ~ + new FilesRouteADM(routeData).makeRoute } } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala index 9317aca03a..9bce509696 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala @@ -13,7 +13,6 @@ import spray.json.JsObject import spray.json.JsString import zio._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.core.State import org.knora.webapi.core.domain.AppState import org.knora.webapi.messages.util.KnoraSystemInstances @@ -115,7 +114,7 @@ trait HealthCheck { /** * Provides the '/health' endpoint serving the health status. */ -final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State], appConfig: AppConfig) +final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State]) extends HealthCheck with Authenticator { @@ -132,7 +131,9 @@ final case class HealthRoute(routeData: KnoraRouteData, runtime: Runtime[State], state <- ZIO.service[State] requestingUser <- ZIO - .fromFuture(_ => getUserADM(requestContext, appConfig)(routeData.system, routeData.appActor, ec)) + .fromFuture(_ => + getUserADM(requestContext, routeData.appConfig)(routeData.system, routeData.appActor, ec) + ) .orElse(ZIO.succeed(KnoraSystemInstances.Users.AnonymousUser)) result <- healthCheck(state) _ <- ZIO.logInfo("health route finished") @@ ZIOAspect.annotated("user-id", requestingUser.id.toString()) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala index 3246ff6bf9..53ee8e19a0 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala @@ -27,22 +27,23 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentif import org.knora.webapi.messages.admin.responder.usersmessages.UserADM /** - * Data needed to be passed to each route. + * Data that needs to be passed to each route. * - * @param system the actor system. - * @param appActor the main application actor. + * @param system the actor system. + * @param appActor the main application actor. + * @param appConfig the application's configuration. */ case class KnoraRouteData(system: akka.actor.ActorSystem, appActor: akka.actor.ActorRef, appConfig: AppConfig) /** * An abstract class providing functionality that is commonly used in implementing Knora routes. * - * @param routeData a [[KnoraRouteData]] providing access to the application. + * @param routeData a [[KnoraRouteData]] providing access to the application. */ -abstract class KnoraRoute(routeData: KnoraRouteData, appConfig: AppConfig) { +abstract class KnoraRoute(routeData: KnoraRouteData) { implicit protected val system: ActorSystem = routeData.system - implicit protected val timeout: Timeout = appConfig.defaultTimeoutAsDuration + implicit protected val timeout: Timeout = routeData.appConfig.defaultTimeoutAsDuration implicit protected val executionContext: ExecutionContext = system.dispatcher implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance implicit protected val appActor: ActorRef = routeData.appActor diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala index 537082c6f6..27cf4a35f0 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala @@ -15,7 +15,6 @@ import scala.concurrent.Future import scala.util.Failure import scala.util.Success -import org.knora.webapi.config.AppConfig import org.knora.webapi.core.State import org.knora.webapi.core.domain.AppState @@ -28,7 +27,7 @@ import org.knora.webapi.core.domain.AppState * * TODO: This should probably be refactored into a ZIO-HTTP middleware, when the transistion to ZIO-HTTP is done. */ -class RejectingRoute(system: akka.actor.ActorSystem, runtime: Runtime[State], appConfig: AppConfig) { self => +class RejectingRoute(routeData: KnoraRouteData, runtime: Runtime[State]) { self => val log: Logger = Logger(this.getClass) @@ -52,7 +51,7 @@ class RejectingRoute(system: akka.actor.ActorSystem, runtime: Runtime[State], ap def makeRoute: Route = path(Remaining) { wholePath => // check to see if route is on the rejection list - val rejectSeq: Seq[Option[Boolean]] = appConfig.routesToReject.map { pathToReject: String => + val rejectSeq: Seq[Option[Boolean]] = routeData.appConfig.routesToReject.map { pathToReject: String => if (wholePath.contains(pathToReject.toCharArray)) { Some(true) } else { diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala index b2abcb9159..52dd0710a3 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala @@ -9,7 +9,6 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.sipimessages.SipiFileInfoGetRequestADM import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -19,9 +18,7 @@ import org.knora.webapi.routing.RouteUtilADM /** * Provides a routing function for the API that Sipi connects to. */ -class FilesRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class FilesRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * A routing function for the API that Sipi connects to. @@ -33,7 +30,7 @@ class FilesRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) path("admin" / "files" / Segments(2)) { projectIDAndFile: Seq[String] => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) projectID = stringFormatter.validateProjectShortcode( projectIDAndFile.head, throw BadRequestException(s"Invalid project ID: '${projectIDAndFile.head}'") diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala index fe903fc108..d60343802d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala @@ -15,7 +15,6 @@ import java.util.UUID import dsp.errors.BadRequestException import dsp.valueobjects.Group._ import dsp.valueobjects.Iri._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.groupsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -26,8 +25,8 @@ import org.knora.webapi.routing.RouteUtilADM * Provides a routing function for API routes that deal with groups. */ -class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class GroupsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with GroupsADMJsonProtocol { @@ -48,7 +47,7 @@ class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) private def getGroups(): Route = path(groupsBasePath) { get { requestContext => val requestMessage = for { - _ <- getUserADM(requestContext, appConfig) + _ <- getUserADM(requestContext, routeData.appConfig) } yield GroupsGetRequestADM() RouteUtilADM.runJsonRoute( @@ -69,7 +68,7 @@ class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid custom group IRI $value")) val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GroupGetRequestADM( groupIri = checkedGroupIri, requestingUser = requestingUser @@ -94,7 +93,7 @@ class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid group IRI $value")) val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GroupMembersGetRequestADM( groupIri = checkedGroupIri, requestingUser = requestingUser @@ -127,7 +126,7 @@ class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { payload <- toFuture(validatedGroupCreatePayload) - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GroupCreateRequestADM( createRequest = payload, requestingUser = requestingUser, @@ -174,7 +173,7 @@ class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { payload <- toFuture(validatedGroupUpdatePayload) - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GroupChangeRequestADM( groupIri = checkedGroupIri, changeGroupRequest = payload, @@ -214,7 +213,7 @@ class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) } val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GroupChangeStatusRequestADM( groupIri = checkedGroupIri, changeGroupRequest = apiRequest, @@ -241,7 +240,7 @@ class GroupsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid group IRI $value")) val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GroupChangeStatusRequestADM( groupIri = checkedGroupIri, changeGroupRequest = ChangeGroupApiRequestADM(status = Some(false)), diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala index 5513df2037..0a8e5e9495 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ListsRouteADM.scala @@ -8,7 +8,6 @@ package org.knora.webapi.routing.admin import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.admin.lists._ @@ -16,11 +15,11 @@ import org.knora.webapi.routing.admin.lists._ /** * Provides an akka-http-routing function for API routes that deal with lists. */ -class ListsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) extends KnoraRoute(routeData, appConfig) { - private val getNodeRoute: GetListItemsRouteADM = new GetListItemsRouteADM(routeData, appConfig) - private val createNodeRoute: CreateListItemsRouteADM = new CreateListItemsRouteADM(routeData, appConfig) - private val deleteNodeRoute: DeleteListItemsRouteADM = new DeleteListItemsRouteADM(routeData, appConfig) - private val updateNodeRoute: UpdateListItemsRouteADM = new UpdateListItemsRouteADM(routeData, appConfig) +class ListsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) { + private val getNodeRoute: GetListItemsRouteADM = new GetListItemsRouteADM(routeData) + private val createNodeRoute: CreateListItemsRouteADM = new CreateListItemsRouteADM(routeData) + private val deleteNodeRoute: DeleteListItemsRouteADM = new DeleteListItemsRouteADM(routeData) + private val updateNodeRoute: UpdateListItemsRouteADM = new UpdateListItemsRouteADM(routeData) override def makeRoute: Route = getNodeRoute.makeRoute ~ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala index 39852b42d9..37fc7edbd2 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/PermissionsRouteADM.scala @@ -8,7 +8,6 @@ package org.knora.webapi.routing.admin import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.admin.permissions._ @@ -16,11 +15,11 @@ import org.knora.webapi.routing.admin.permissions._ /** * Provides an akka-http-routing function for API routes that deal with permissions. */ -class PermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) extends KnoraRoute(routeData, appConfig) { - private val createPermissionRoute: CreatePermissionRouteADM = new CreatePermissionRouteADM(routeData, appConfig) - private val getPermissionRoute: GetPermissionsRouteADM = new GetPermissionsRouteADM(routeData, appConfig) - private val updatePermissionRoute: UpdatePermissionRouteADM = new UpdatePermissionRouteADM(routeData, appConfig) - private val deletePermissionRoute: DeletePermissionRouteADM = new DeletePermissionRouteADM(routeData, appConfig) +class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) { + private val createPermissionRoute: CreatePermissionRouteADM = new CreatePermissionRouteADM(routeData) + private val getPermissionRoute: GetPermissionsRouteADM = new GetPermissionsRouteADM(routeData) + private val updatePermissionRoute: UpdatePermissionRouteADM = new UpdatePermissionRouteADM(routeData) + private val deletePermissionRoute: DeletePermissionRouteADM = new DeletePermissionRouteADM(routeData) override def makeRoute: Route = createPermissionRoute.makeRoute ~ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala index cdbde942d4..a00a3e6aa4 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala @@ -29,14 +29,13 @@ import dsp.errors.BadRequestException import dsp.valueobjects.Iri.ProjectIri import dsp.valueobjects.Project._ import org.knora.webapi.IRI -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class ProjectsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with ProjectsADMJsonProtocol { @@ -73,7 +72,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectsGetRequestADM( requestingUser = requestingUser @@ -109,7 +108,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectCreateRequestADM] = for { projectCreatePayload <- toFuture(projectCreatePayload) - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield ProjectCreateRequestADM( createRequest = projectCreatePayload, requestingUser = requestingUser, @@ -132,7 +131,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectsKeywordsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectsKeywordsGetRequestADM( requestingUser = requestingUser @@ -157,7 +156,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectKeywordsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectKeywordsGetRequestADM( projectIri = checkedProjectIri, @@ -182,7 +181,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -210,7 +209,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) shortNameDec = stringFormatter.validateAndEscapeProjectShortname( value, @@ -240,7 +239,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) checkedShortcode = stringFormatter.validateAndEscapeProjectShortcode( value, @@ -276,7 +275,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectChangeRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectChangeRequestADM( projectIri = checkedProjectIri, @@ -307,7 +306,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectChangeRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectChangeRequestADM( projectIri = checkedProjectIri, @@ -334,7 +333,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -362,7 +361,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) shortNameDec = stringFormatter.validateAndEscapeProjectShortname( value, @@ -392,7 +391,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) checkedShortcode = stringFormatter.validateAndEscapeProjectShortcode( value, @@ -422,7 +421,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -450,7 +449,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) checkedShortname = stringFormatter.validateAndEscapeProjectShortname( value, @@ -480,7 +479,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) checkedShortcode = stringFormatter.validateProjectShortcode( value, @@ -510,7 +509,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectRestrictedViewSettingsGetRequestADM( @@ -536,7 +535,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) shortNameDec = java.net.URLDecoder.decode(value, "utf-8") @@ -563,7 +562,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectRestrictedViewSettingsGetRequestADM( identifier = ProjectIdentifierADM(maybeShortcode = Some(value)), @@ -602,7 +601,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val httpEntityFuture: Future[HttpEntity.Chunked] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage = ProjectDataGetRequestADM( diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala index 039512d22c..e539bfb2ea 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/StoreRouteADM.scala @@ -11,7 +11,6 @@ import akka.http.scaladsl.server.Route import scala.concurrent.Future import scala.concurrent.duration._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.storesmessages.ResetTriplestoreContentRequestADM import org.knora.webapi.messages.admin.responder.storesmessages.StoresADMJsonProtocol import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject @@ -24,8 +23,8 @@ import org.knora.webapi.routing.RouteUtilADM * A route used to send requests which can directly affect the data stored inside the triplestore. */ -class StoreRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class StoreRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with StoresADMJsonProtocol { diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala index 03701df12c..4b884c28b5 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala @@ -17,7 +17,6 @@ import dsp.errors.BadRequestException import dsp.valueobjects.Iri.UserIri import dsp.valueobjects.LanguageCode import dsp.valueobjects.User._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.usersmessages.UsersADMJsonProtocol._ import org.knora.webapi.messages.admin.responder.usersmessages._ import org.knora.webapi.messages.util.KnoraSystemInstances @@ -29,9 +28,7 @@ import org.knora.webapi.routing.RouteUtilADM /** * Provides an akka-http-routing function for API routes that deal with users. */ -class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { val usersBasePath: PathMatcher[Unit] = PathMatcher("admin" / "users") @@ -65,7 +62,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UsersGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UsersGetRequestADM( requestingUser = requestingUser @@ -114,7 +111,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserCreateRequestADM] = for { payload <- toFuture(validatedUserCreatePayload) - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield UserCreateRequestADM( userCreatePayloadADM = payload, requestingUser = requestingUser, @@ -139,7 +136,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserGetRequestADM( identifier = UserIdentifierADM(maybeIri = Some(userIri)), @@ -165,7 +162,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserGetRequestADM( identifier = UserIdentifierADM(maybeEmail = Some(userIri)), @@ -191,7 +188,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserGetRequestADM( identifier = UserIdentifierADM(maybeUsername = Some(userIri)), @@ -266,7 +263,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserChangeBasicInformationRequestADM( userIri = checkedUserIri, @@ -317,7 +314,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserChangePasswordRequestADM( userIri = checkedUserIri, @@ -364,7 +361,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserChangeStatusRequestADM( userIri = checkedUserIri, @@ -407,7 +404,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserChangeStatusRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserChangeStatusRequestADM( userIri = checkedUserIri, @@ -453,7 +450,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserChangeSystemAdminMembershipStatusRequestADM( userIri = checkedUserIri, @@ -486,7 +483,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserProjectMembershipsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserProjectMembershipsGetRequestADM( userIri = checkedUserIri, @@ -530,7 +527,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserProjectMembershipAddRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserProjectMembershipAddRequestADM( userIri = checkedUserIri, @@ -576,7 +573,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserProjectMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserProjectMembershipRemoveRequestADM( userIri = checkedUserIri, @@ -608,7 +605,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserProjectAdminMembershipsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserProjectAdminMembershipsGetRequestADM( userIri = checkedUserIri, @@ -653,7 +650,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserProjectAdminMembershipAddRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserProjectAdminMembershipAddRequestADM( userIri = checkedUserIri, @@ -699,7 +696,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserProjectAdminMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserProjectAdminMembershipRemoveRequestADM( userIri = checkedUserIri, @@ -731,7 +728,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserGroupMembershipsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserGroupMembershipsGetRequestADM( userIri = checkedUserIri, @@ -772,7 +769,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserGroupMembershipAddRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserGroupMembershipAddRequestADM( userIri = checkedUserIri, @@ -815,7 +812,7 @@ class UsersRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[UserGroupMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield UserGroupMembershipRemoveRequestADM( userIri = checkedUserIri, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala index ce0a5e0b33..27bc48cdec 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala @@ -18,7 +18,6 @@ import dsp.errors.ForbiddenException import dsp.valueobjects.Iri._ import dsp.valueobjects.List._ import dsp.valueobjects.ListErrorMessages -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListRootNodeCreatePayloadADM import org.knora.webapi.messages.admin.responder.listsmessages._ @@ -32,8 +31,8 @@ import org.knora.webapi.routing.RouteUtilADM * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ -class CreateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class CreateListItemsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with ListADMJsonProtocol { @@ -62,7 +61,7 @@ class CreateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ListRootNodeCreateRequestADM] = for { payload <- toFuture(validatedListRootNodeCreatePayload) - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) // check if the requesting user is allowed to perform operation _ = @@ -116,7 +115,7 @@ class CreateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ListChildNodeCreateRequestADM] = for { payload <- toFuture(validatedCreateChildNodePeyload) - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) // check if the requesting user is allowed to perform operation _ = diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala index 630d103313..3372be2583 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala @@ -13,7 +13,6 @@ import java.util.UUID import scala.concurrent.Future import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -25,8 +24,8 @@ import org.knora.webapi.routing.RouteUtilADM * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ -class DeleteListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class DeleteListItemsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with ListADMJsonProtocol { @@ -46,7 +45,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list item Iri: $iri")) val requestMessage: Future[ListItemDeleteRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield ListItemDeleteRequestADM( nodeIri = nodeIri, requestingUser = requestingUser, @@ -72,7 +71,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list IRI: $iri")) val requestMessage: Future[CanDeleteListRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield CanDeleteListRequestADM( iri = listIri, requestingUser = requestingUser @@ -97,7 +96,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ListNodeCommentsDeleteRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield ListNodeCommentsDeleteRequestADM( iri = listIri, requestingUser = requestingUser diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala index 5253035376..bff986f34b 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala @@ -13,7 +13,6 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi.IRI -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -25,8 +24,8 @@ import org.knora.webapi.routing.RouteUtilADM * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ -class GetListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class GetListItemsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with ListADMJsonProtocol { @@ -54,7 +53,7 @@ class GetListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ListsGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ListsGetRequestADM( projectIri = projectIri, @@ -82,7 +81,7 @@ class GetListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ListGetRequestADM] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ListGetRequestADM( iri = listIri, @@ -107,7 +106,7 @@ class GetListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) val requestMessage: Future[ListNodeInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield ListNodeInfoGetRequestADM( iri = listIri, requestingUser = requestingUser @@ -133,7 +132,7 @@ class GetListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) val requestMessage: Future[ListNodeInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield ListNodeInfoGetRequestADM( iri = listIri, requestingUser = requestingUser diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala index 692531a17a..0cedc75d74 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala @@ -18,7 +18,6 @@ import dsp.errors.ForbiddenException import dsp.valueobjects.Iri._ import dsp.valueobjects.List._ import dsp.valueobjects.ListErrorMessages -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -30,8 +29,8 @@ import org.knora.webapi.routing.RouteUtilADM * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ -class UpdateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class UpdateListItemsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with ListADMJsonProtocol { @@ -58,7 +57,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) NodeNameChangePayloadADM(ListName.make(apiRequest.name).fold(e => throw e.head, v => v)) val requestMessage: Future[NodeNameChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield NodeNameChangeRequestADM( nodeIri = nodeIri, changeNodeNameRequest = namePayload, @@ -90,7 +89,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) NodeLabelsChangePayloadADM(Labels.make(apiRequest.labels).fold(e => throw e.head, v => v)) val requestMessage: Future[NodeLabelsChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield NodeLabelsChangeRequestADM( nodeIri = nodeIri, changeNodeLabelsRequest = labelsPayload, @@ -122,7 +121,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) NodeCommentsChangePayloadADM(Comments.make(apiRequest.comments).fold(e => throw e.head, v => v)) val requestMessage: Future[NodeCommentsChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield NodeCommentsChangeRequestADM( nodeIri = nodeIri, changeNodeCommentsRequest = commentsPayload, @@ -151,7 +150,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param node IRI: $iri")) val requestMessage: Future[NodePositionChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield NodePositionChangeRequestADM( nodeIri = nodeIri, changeNodePositionRequest = apiRequest, @@ -198,7 +197,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[NodeInfoChangeRequestADM] = for { payload <- toFuture(validatedChangeNodeInfoPayload) - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) // check if the requesting user is allowed to perform operation _ = if ( !requestingUser.permissions.isProjectAdmin( diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala index c88e474c38..8933cb67e1 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala @@ -12,14 +12,13 @@ import akka.http.scaladsl.server.Route import java.util.UUID import scala.concurrent.Future -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -class CreatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class CreatePermissionRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with PermissionsADMJsonProtocol { @@ -41,7 +40,7 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) /* create a new administrative permission */ entity(as[CreateAdministrativePermissionAPIRequestADM]) { apiRequest => requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield AdministrativePermissionCreateRequestADM( createRequest = apiRequest, requestingUser = requestingUser, @@ -67,7 +66,7 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) /* create a new default object access permission */ entity(as[CreateDefaultObjectAccessPermissionAPIRequestADM]) { apiRequest => requestContext => val requestMessage: Future[DefaultObjectAccessPermissionCreateRequestADM] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield DefaultObjectAccessPermissionCreateRequestADM( createRequest = apiRequest, requestingUser = requestingUser, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala index 40219a993e..b9000a6f5a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala @@ -11,14 +11,13 @@ import akka.http.scaladsl.server.Route import java.util.UUID -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -class DeletePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class DeletePermissionRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with PermissionsADMJsonProtocol { @@ -37,7 +36,7 @@ class DeletePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) path(permissionsBasePath / Segment) { iri => delete { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield PermissionDeleteRequestADM( permissionIri = iri, requestingUser = requestingUser, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala index 729d42fa53..53f1088810 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala @@ -11,14 +11,13 @@ import akka.http.scaladsl.server.Route import java.util.UUID -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -class GetPermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class GetPermissionsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with PermissionsADMJsonProtocol { @@ -37,7 +36,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) path(permissionsBasePath / "ap" / Segment / Segment) { (projectIri, groupIri) => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield AdministrativePermissionForProjectGroupGetRequestADM(projectIri, groupIri, requestingUser) RouteUtilADM.runJsonRoute( @@ -53,7 +52,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) path(permissionsBasePath / "ap" / Segment) { projectIri => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield AdministrativePermissionsForProjectGetRequestADM( projectIri = projectIri, requestingUser = requestingUser, @@ -73,7 +72,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) path(permissionsBasePath / "doap" / Segment) { projectIri => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield DefaultObjectAccessPermissionsForProjectGetRequestADM( projectIri = projectIri, requestingUser = requestingUser, @@ -93,7 +92,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) path(permissionsBasePath / Segment) { projectIri => get { requestContext => val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield PermissionsForProjectGetRequestADM( projectIri = projectIri, requestingUser = requestingUser, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala index e8a89d962d..79517ba13f 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala @@ -12,14 +12,13 @@ import akka.http.scaladsl.server.Route import java.util.UUID import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -class UpdatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class UpdatePermissionRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with PermissionsADMJsonProtocol { @@ -45,7 +44,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield PermissionChangeGroupRequestADM( permissionIri = permissionIri, changePermissionGroupRequest = apiRequest, @@ -74,7 +73,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield PermissionChangeHasPermissionsRequestADM( permissionIri = permissionIri, changePermissionHasPermissionsRequest = apiRequest, @@ -103,7 +102,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield PermissionChangeResourceClassRequestADM( permissionIri = permissionIri, changePermissionResourceClassRequest = apiRequest, @@ -132,7 +131,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData, appConfig: AppConfig) stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid permission IRI: $iri")) val requestMessage = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield PermissionChangePropertyRequestADM( permissionIri = permissionIri, changePermissionPropertyRequest = apiRequest, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala index dfebd12ca5..a88a6cc238 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/AssetsRouteV1.scala @@ -19,7 +19,6 @@ import java.io.ByteArrayOutputStream import java.nio.file.Paths import javax.imageio.ImageIO -import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData @@ -27,9 +26,7 @@ import org.knora.webapi.routing.KnoraRouteData /** * A route used for faking the image server. */ -class AssetsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class AssetsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala index 70534a891a..b52acdbeb3 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/AuthenticationRouteV1.scala @@ -8,7 +8,6 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData @@ -16,9 +15,7 @@ import org.knora.webapi.routing.KnoraRouteData /** * A route providing authentication support. It allows the creation of "sessions", which is used in the SALSAH app. */ -class AuthenticationRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class AuthenticationRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -27,7 +24,7 @@ class AuthenticationRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) path("v1" / "authenticate") { get { requestContext => requestContext.complete { - doAuthenticateV1(requestContext, appConfig) + doAuthenticateV1(requestContext, routeData.appConfig) } } } ~ path("v1" / "session") { @@ -35,20 +32,20 @@ class AuthenticationRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) requestContext.complete { val params = requestContext.request.uri.query().toMap if (params.contains("logout")) { - doLogoutV2(requestContext, appConfig) + doLogoutV2(requestContext, routeData.appConfig) } else if (params.contains("login")) { - doLoginV1(requestContext, appConfig) + doLoginV1(requestContext, routeData.appConfig) } else { - doAuthenticateV1(requestContext, appConfig) + doAuthenticateV1(requestContext, routeData.appConfig) } } } ~ post { requestContext => requestContext.complete { - doLoginV1(requestContext, appConfig) + doLoginV1(requestContext, routeData.appConfig) } } ~ delete { requestContext => requestContext.complete { - doLogoutV2(requestContext, appConfig) + doLogoutV2(requestContext, routeData.appConfig) } } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala index f558803b83..0aea545450 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/CkanRouteV1.scala @@ -8,7 +8,6 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.ckanmessages.CkanRequestV1 import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -18,9 +17,7 @@ import org.knora.webapi.routing.RouteUtilV1 /** * A route used to serve data to CKAN. It is used be the Ckan instance running under http://data.humanities.ch. */ -class CkanRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class CkanRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -29,7 +26,7 @@ class CkanRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) path("v1" / "ckan") { get { requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext, appConfig) + userProfile <- getUserADM(requestContext, routeData.appConfig) params = requestContext.request.uri.query().toMap project: Option[Seq[String]] = params.get("project").map(_.split(",").toSeq) limit: Option[Int] = params.get("limit").map(_.toInt) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala index c0c4a88972..d1c32280fa 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ListsRouteV1.scala @@ -9,7 +9,6 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.v1.responder.listmessages._ import org.knora.webapi.routing.Authenticator @@ -20,9 +19,7 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides API routes that deal with lists. */ -class ListsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class ListsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -34,7 +31,7 @@ class ListsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) path("v1" / "hlists" / Segment) { iri => get { requestContext => val requestMessageFuture = for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) listIri = stringFormatter.validateAndEscapeIri( iri, throw BadRequestException(s"Invalid param list IRI: $iri") @@ -58,7 +55,7 @@ class ListsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) path("v1" / "selections" / Segment) { iri => get { requestContext => val requestMessageFuture = for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) selIri = stringFormatter.validateAndEscapeIri( iri, throw BadRequestException(s"Invalid param list IRI: $iri") diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala index 100b865e31..455e569316 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ProjectsRouteV1.scala @@ -9,15 +9,14 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.projectmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilV1 -class ProjectsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class ProjectsRouteV1(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with ProjectV1JsonProtocol { @@ -30,7 +29,7 @@ class ProjectsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) /* returns all projects */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) } yield ProjectsGetRequestV1( userProfile = Some(userProfile) ) @@ -49,7 +48,7 @@ class ProjectsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = if (identifier != "iri") { // identify project by shortname. val shortNameDec = java.net.URLDecoder.decode(value, "utf-8") for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) } yield ProjectInfoByShortnameGetRequestV1( shortname = shortNameDec, userProfileV1 = Some(userProfile) @@ -58,7 +57,7 @@ class ProjectsRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val checkedProjectIri = stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid project IRI $value")) for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) } yield ProjectInfoByIRIGetRequestV1( iri = checkedProjectIri, userProfileV1 = Some(userProfile) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala index 44c9921e7a..14005e5742 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourceTypesRouteV1.scala @@ -9,7 +9,6 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.ontologymessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -19,9 +18,7 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides a spray-routing function for API routes that deal with resource types. */ -class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -31,7 +28,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) path("v1" / "resourcetypes" / Segment) { iri => get { requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext, appConfig) + userProfile <- getUserADM(requestContext, routeData.appConfig) // TODO: Check that this is the IRI of a resource type and not just any IRI resourceTypeIri = @@ -52,7 +49,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) } ~ path("v1" / "resourcetypes") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext, appConfig) + userADM <- getUserADM(requestContext, routeData.appConfig) params = requestContext.request.uri.query().toMap vocabularyId = params.getOrElse( @@ -87,7 +84,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) } ~ path("v1" / "propertylists") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext, appConfig) + userADM <- getUserADM(requestContext, routeData.appConfig) params = requestContext.request.uri.query().toMap vocabularyId: Option[String] = params.get("vocabulary") @@ -140,7 +137,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) } ~ path("v1" / "vocabularies") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext, appConfig) + userADM <- getUserADM(requestContext, routeData.appConfig) } yield NamedGraphsGetRequestV1(userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( @@ -154,7 +151,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) } ~ path("v1" / "vocabularies" / "reload") { get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext, appConfig) + userADM <- getUserADM(requestContext, routeData.appConfig) } yield LoadOntologiesRequestV1(userADM = userADM) RouteUtilV1.runJsonRouteWithFuture( @@ -167,7 +164,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) } ~ path("v1" / "subclasses" / Segment) { iri => get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext, appConfig) + userADM <- getUserADM(requestContext, routeData.appConfig) // TODO: Check that this is the IRI of a resource type and not just any IRI resourceClassIri = stringFormatter.validateAndEscapeIri( diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala index 7d8b6b9412..eab5739cfb 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala @@ -36,7 +36,6 @@ import dsp.errors.BadRequestException import dsp.errors.ForbiddenException import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -64,9 +63,7 @@ import org.knora.webapi.util.FileUtil /** * Provides API routes that deal with resources. */ -class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { // A scala.xml.PrettyPrinter for formatting generated XML import schemas. private val xmlPrettyPrinter = new scala.xml.PrettyPrinter(width = 160, step = 4) @@ -1219,7 +1216,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userProfile <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) params = requestContext.request.uri.query().toMap searchstr = params.getOrElse("searchstr", throw BadRequestException(s"required param searchstr is missing")) @@ -1283,7 +1280,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture = for { userProfile <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) request <- makeCreateResourceRequestMessage( apiRequest = apiRequest, @@ -1306,7 +1303,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestType = reqtypeParam.getOrElse("") resinfo = resinfoParam.getOrElse(false) @@ -1329,7 +1326,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield makeResourceDeleteMessage(resIri = resIri, deleteComment = deleteCommentParam, userADM = userADM) @@ -1351,7 +1348,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) resIri = stringFormatter.validateAndEscapeIri( iri, @@ -1377,7 +1374,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) resIri = stringFormatter.validateAndEscapeIri( iri, @@ -1399,7 +1396,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) resIri = stringFormatter.validateAndEscapeIri( iri, @@ -1430,7 +1427,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) resourceIri = stringFormatter.validateAndEscapeIri( iri, @@ -1470,7 +1467,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) _ = if (userADM.isAnonymousUser) { @@ -1525,7 +1522,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) requestContext = requestContext, appActor = appActor, log = log - )(timeout = appConfig.defaultTimeoutAsDuration, executionContext = executionContext) + )(timeout = routeData.appConfig.defaultTimeoutAsDuration, executionContext = executionContext) } } } ~ path("v1" / "resources" / "xmlimportschemas" / Segment) { internalOntologyIri => @@ -1551,7 +1548,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val httpResponseFuture: Future[HttpResponse] = for { userProfile <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) schemaZipFileBytes: Array[Byte] <- generateSchemaZipFile( internalOntologyIri = internalOntologyIri, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala index 666cea4060..29ae083cb2 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/SearchRouteV1.scala @@ -12,7 +12,6 @@ import scala.language.postfixOps import dsp.errors.BadRequestException import org.knora.webapi.IRI -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.v1.responder.searchmessages.ExtendedSearchGetRequestV1 @@ -28,9 +27,7 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides a spray-routing function for API routes that deal with search. */ -class SearchRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class SearchRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * The default number of rows to show in search results. @@ -250,7 +247,7 @@ class SearchRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) // in the original API, there is a slash after "search": "http://www.salsah.org/api/search/?searchtype=extended" get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext, appConfig) + userADM <- getUserADM(requestContext, routeData.appConfig) params: Map[String, Seq[String]] = requestContext.request.uri.query().toMultiMap } yield makeExtendedSearchRequestMessage(userADM, params) @@ -266,7 +263,7 @@ class SearchRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => val requestMessage = for { - userADM <- getUserADM(requestContext, appConfig) + userADM <- getUserADM(requestContext, routeData.appConfig) params: Map[String, String] = requestContext.request.uri.query().toMap } yield makeFulltextSearchRequestMessage(userADM, searchval, params) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala index ebcdc501c5..a3735f7813 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/StandoffRouteV1.scala @@ -16,7 +16,6 @@ import scala.concurrent.Future import scala.concurrent.duration._ import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.standoffmessages.RepresentationV1JsonProtocol.createMappingApiRequestV1Format import org.knora.webapi.messages.v1.responder.standoffmessages._ import org.knora.webapi.routing.Authenticator @@ -27,9 +26,7 @@ import org.knora.webapi.routing.RouteUtilV1 /** * A route used to convert XML to standoff. */ -class StandoffRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class StandoffRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -70,7 +67,7 @@ class StandoffRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CreateMappingRequestV1] = for { - userProfile <- getUserADM(requestContext, appConfig) + userProfile <- getUserADM(requestContext, routeData.appConfig) allParts: Map[Name, String] <- allPartsFuture diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala index 73e1d98a75..2afe0e7905 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/UsersRouteV1.scala @@ -12,7 +12,6 @@ import org.apache.commons.validator.routines.UrlValidator import java.util.UUID import dsp.errors.BadRequestException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v1.responder.usermessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute @@ -22,9 +21,7 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides a spray-routing function for API routes that deal with lists. */ -class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { private val schemes = Array("http", "https") new UrlValidator(schemes) @@ -39,7 +36,7 @@ class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) /* return all users */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) } yield UsersGetRequestV1(userProfile) RouteUtilV1.runJsonRouteWithFuture( @@ -57,7 +54,7 @@ class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) /* check if email or iri was supplied */ val requestMessage = if (identifier == "email") { for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) } yield UserProfileByEmailGetRequestV1( email = value, userProfileType = UserProfileTypeV1.RESTRICTED, @@ -65,7 +62,7 @@ class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) ) } else { for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) userIri = stringFormatter.validateAndEscapeIri( value, throw BadRequestException(s"Invalid user IRI $value") @@ -91,7 +88,7 @@ class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) /* get user's project memberships */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) checkedUserIri = stringFormatter.validateAndEscapeIri( userIri, throw BadRequestException(s"Invalid user IRI $userIri") @@ -115,7 +112,7 @@ class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) /* get user's project admin memberships */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) checkedUserIri = stringFormatter.validateAndEscapeIri( userIri, throw BadRequestException(s"Invalid user IRI $userIri") @@ -139,7 +136,7 @@ class UsersRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) /* get user's group memberships */ requestContext => val requestMessage = for { - userProfile <- getUserADM(requestContext, appConfig).map(_.asUserProfileV1) + userProfile <- getUserADM(requestContext, routeData.appConfig).map(_.asUserProfileV1) checkedUserIri = stringFormatter.validateAndEscapeIri( userIri, throw BadRequestException(s"Invalid user IRI $userIri") diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala index 93882bae22..16fd0654c9 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ValuesRouteV1.scala @@ -18,7 +18,6 @@ import dsp.errors.BadRequestException import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages.GetFileMetadataRequest @@ -37,9 +36,7 @@ import org.knora.webapi.routing.RouteUtilV1 /** * Provides an Akka routing function for API routes that deal with values. */ -class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -613,7 +610,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield makeVersionHistoryRequestMessage(iris = iris, userADM = userADM) @@ -630,7 +627,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) request <- makeCreateValueRequestMessage(apiRequest = apiRequest, userADM = userADM) } yield request @@ -648,7 +645,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield makeGetValueRequest(valueIriStr = valueIriStr, userADM = userADM) @@ -665,7 +662,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) request <- apiRequest match { case ChangeValueApiRequestV1(_, _, _, _, _, _, _, _, _, _, _, _, _, Some(comment)) => @@ -696,7 +693,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) params = requestContext.request.uri.query().toMap deleteComment = params.get("deleteComment") @@ -714,7 +711,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield makeChangeCommentRequestMessage(valueIriStr = valueIriStr, comment = None, userADM = userADM) @@ -731,7 +728,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield makeLinkValueGetRequestMessage(iris = iris, userADM = userADM) @@ -748,7 +745,7 @@ class ValuesRouteV1(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage = for { userADM <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) resourceIri = stringFormatter.validateAndEscapeIri( resIriStr, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala index e983c6dea6..72acdcf6c3 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/AuthenticationRouteV2.scala @@ -8,7 +8,6 @@ package org.knora.webapi.routing.v2 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.usersmessages.UserIdentifierADM import org.knora.webapi.messages.v2.routing.authenticationmessages.AuthenticationV2JsonProtocol import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraPasswordCredentialsV2 @@ -20,8 +19,8 @@ import org.knora.webapi.routing.KnoraRouteData /** * A route providing API v2 authentication support. It allows the creation of "sessions", which are used in the SALSAH app. */ -class AuthenticationRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) +class AuthenticationRouteV2(routeData: KnoraRouteData) + extends KnoraRoute(routeData) with Authenticator with AuthenticationV2JsonProtocol { @@ -35,7 +34,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) requestContext.complete { doAuthenticateV2( requestContext = requestContext, - appConfig + routeData.appConfig ) } } ~ @@ -64,7 +63,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) ), password = apiRequest.password ), - appConfig + routeData.appConfig ) } } @@ -72,7 +71,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) delete { // logout requestContext => requestContext.complete { - doLogoutV2(requestContext, appConfig) + doLogoutV2(requestContext, routeData.appConfig) } } } ~ @@ -80,7 +79,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) get { // html login interface (necessary for IIIF Authentication API support) requestContext => requestContext.complete { - presentLoginFormV2(requestContext, appConfig) + presentLoginFormV2(requestContext, routeData.appConfig) } } ~ post { // called by html login interface (necessary for IIIF Authentication API support) @@ -94,7 +93,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) ), password = password ), - appConfig + routeData.appConfig ) } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala index a9f85d719c..e1c8e54145 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ListsRouteV2.scala @@ -12,7 +12,6 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.v2.responder.listsmessages.ListGetRequestV2 import org.knora.webapi.messages.v2.responder.listsmessages.NodeGetRequestV2 import org.knora.webapi.routing.Authenticator @@ -23,9 +22,7 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a function for API routes that deal with lists and nodes. */ -class ListsRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -41,7 +38,7 @@ class ListsRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[ListGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) listIri: IRI = stringFormatter.validateAndEscapeIri( lIri, @@ -55,7 +52,7 @@ class ListsRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -71,7 +68,7 @@ class ListsRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessage: Future[NodeGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) nodeIri: IRI = stringFormatter.validateAndEscapeIri( nIri, @@ -85,7 +82,7 @@ class ListsRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, 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 fc9c3fecc9..e2108026e7 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 @@ -23,7 +23,6 @@ import dsp.valueobjects.LangString import dsp.valueobjects.Schema._ import org.knora.webapi.ApiV2Complex import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -41,9 +40,7 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a routing function for API v2 routes that deal with ontologies. */ -class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { val ontologiesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "ontologies") @@ -88,7 +85,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) // This is the route used to dereference an actual ontology IRI. If the URL path looks like it // belongs to a built-in API ontology (which has to contain "knora-api"), prefix it with // http://api.knora.org to get the ontology IRI. Otherwise, if it looks like it belongs to a - // project-specific API ontology, prefix it with appConfig.externalOntologyIriHostAndPort to get the + // project-specific API ontology, prefix it with routeData.appConfig.externalOntologyIriHostAndPort to get the // ontology IRI. val urlPath = requestContext.request.uri.path.toString @@ -96,7 +93,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestedOntologyStr: IRI = if (stringFormatter.isBuiltInApiV2OntologyUrlPath(urlPath)) { OntologyConstants.KnoraApi.ApiOntologyHostname + urlPath } else if (stringFormatter.isProjectSpecificApiV2OntologyUrlPath(urlPath)) { - "http://" + appConfig.knoraApi.externalOntologyIriHostAndPort + urlPath + "http://" + routeData.appConfig.knoraApi.externalOntologyIriHostAndPort + urlPath } else { throw BadRequestException(s"Invalid or unknown URL path for external ontology: $urlPath") } @@ -122,7 +119,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[OntologyEntitiesGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield OntologyEntitiesGetRequestV2( ontologyIri = requestedOntology, @@ -133,7 +130,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -150,7 +147,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[OntologyMetadataGetByProjectRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield OntologyMetadataGetByProjectRequestV2( projectIris = maybeProjectIri.toSet, @@ -160,7 +157,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -179,7 +176,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ChangeOntologyMetadataRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: ChangeOntologyMetadataRequestV2 <- ChangeOntologyMetadataRequestV2.fromJsonLD( @@ -194,7 +191,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -211,7 +208,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[OntologyMetadataGetByProjectRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) validatedProjectIris = @@ -226,7 +223,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -259,7 +256,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[OntologyEntitiesGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield OntologyEntitiesGetRequestV2( ontologyIri = requestedOntologyIri, @@ -270,7 +267,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -287,7 +284,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CreateClassRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -304,7 +301,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -324,7 +321,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ChangeClassLabelsOrCommentsRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -341,7 +338,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -375,7 +372,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteClassCommentRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield DeleteClassCommentRequestV2( classIri = classIri, @@ -387,7 +384,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -405,7 +402,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[AddCardinalitiesToClassRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -422,7 +419,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -446,7 +443,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CanChangeCardinalitiesRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield CanChangeCardinalitiesRequestV2( classIri = classIri, @@ -456,7 +453,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -475,7 +472,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ChangeCardinalitiesRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -492,7 +489,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -511,7 +508,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CanDeleteCardinalitiesFromClassRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -529,7 +526,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -550,7 +547,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteCardinalitiesFromClassRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -567,7 +564,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -587,7 +584,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ChangeGuiOrderRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -604,7 +601,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -660,7 +657,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ClassesGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ClassesGetRequestV2( classIris = classesForResponder, @@ -671,7 +668,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -693,7 +690,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CanDeleteClassRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield CanDeleteClassRequestV2( classIri = classIri, @@ -703,7 +700,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -739,7 +736,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteClassRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield DeleteClassRequestV2( classIri = classIri, @@ -751,7 +748,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -782,7 +779,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteOntologyCommentRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield DeleteOntologyCommentRequestV2( ontologyIri = ontologyIri, @@ -794,7 +791,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -811,7 +808,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) { val requestMessageFuture: Future[CreatePropertyRequestV2] = for { - requestingUser: UserADM <- getUserADM(requestContext = requestContext, appConfig) + requestingUser: UserADM <- getUserADM(requestContext = requestContext, routeData.appConfig) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -1004,7 +1001,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1024,7 +1021,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ChangePropertyLabelsOrCommentsRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -1042,7 +1039,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1076,7 +1073,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeletePropertyCommentRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield DeletePropertyCommentRequestV2( propertyIri = propertyIri, @@ -1088,7 +1085,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1105,7 +1102,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) { val requestMessageFuture: Future[ChangePropertyGuiElementRequest] = for { - requestingUser: UserADM <- getUserADM(requestContext = requestContext, appConfig) + requestingUser: UserADM <- getUserADM(requestContext = requestContext, routeData.appConfig) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -1168,7 +1165,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1224,7 +1221,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[PropertiesGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield PropertiesGetRequestV2( propertyIris = propsForResponder, @@ -1235,7 +1232,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -1257,7 +1254,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CanDeletePropertyRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield CanDeletePropertyRequestV2( propertyIri = propertyIri, @@ -1267,7 +1264,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1303,7 +1300,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeletePropertyRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield DeletePropertyRequestV2( propertyIri = propertyIri, @@ -1315,7 +1312,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1332,7 +1329,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CreateOntologyRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest) @@ -1349,7 +1346,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1373,7 +1370,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CanDeleteOntologyRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield CanDeleteOntologyRequestV2( ontologyIri = ontologyIri, @@ -1383,7 +1380,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -1416,7 +1413,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteOntologyRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield DeleteOntologyRequestV2( ontologyIri = ontologyIri, @@ -1428,7 +1425,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index f513cbf885..a19d92ab18 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -15,7 +15,6 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter @@ -38,9 +37,7 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a routing function for API v2 routes that deal with resources. */ -class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") @@ -85,7 +82,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ResourceIIIFManifestGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ResourceIIIFManifestGetRequestV2( resourceIri = resourceIri, @@ -95,7 +92,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -113,7 +110,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CreateResourceRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: CreateResourceRequestV2 <- CreateResourceRequestV2.fromJsonLD( @@ -133,7 +130,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -153,7 +150,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[UpdateResourceMetadataRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: UpdateResourceMetadataRequestV2 <- UpdateResourceMetadataRequestV2.fromJsonLD( @@ -168,7 +165,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -222,7 +219,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[SearchResourcesByProjectAndClassRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield SearchResourcesByProjectAndClassRequestV2( projectIri = projectIri, @@ -237,7 +234,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -270,7 +267,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ResourceVersionHistoryGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ResourceVersionHistoryGetRequestV2( resourceIri = resourceIri, @@ -282,7 +279,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -297,7 +294,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ResourceHistoryEventsGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ResourceHistoryEventsGetRequestV2( resourceIri = resourceIri, @@ -307,7 +304,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -322,7 +319,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ProjectResourcesWithHistoryGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ProjectResourcesWithHistoryGetRequestV2( projectIri = projectIri, @@ -332,7 +329,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -343,9 +340,9 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) private def getResources(): Route = path(resourcesBasePath / Segments) { resIris: Seq[String] => get { requestContext => - if (resIris.size > appConfig.v2.resourcesSequence.resultsPerPage) + if (resIris.size > routeData.appConfig.v2.resourcesSequence.resultsPerPage) throw BadRequestException( - s"List of provided resource Iris exceeds limit of ${appConfig.v2.resourcesSequence.resultsPerPage}" + s"List of provided resource Iris exceeds limit of ${routeData.appConfig.v2.resourcesSequence.resultsPerPage}" ) val resourceIris: Seq[IRI] = resIris.map { resIri: String => @@ -373,7 +370,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ResourcesGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ResourcesGetRequestV2( resourceIris = resourceIris, @@ -386,7 +383,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -398,9 +395,9 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) private def getResourcesPreview(): Route = path("v2" / "resourcespreview" / Segments) { resIris: Seq[String] => get { requestContext => - if (resIris.size > appConfig.v2.resourcesSequence.resultsPerPage) + if (resIris.size > routeData.appConfig.v2.resourcesSequence.resultsPerPage) throw BadRequestException( - s"List of provided resource Iris exceeds limit of ${appConfig.v2.resourcesSequence.resultsPerPage}" + s"List of provided resource Iris exceeds limit of ${routeData.appConfig.v2.resourcesSequence.resultsPerPage}" ) val resourceIris: Seq[IRI] = resIris.map { resIri: String => @@ -412,7 +409,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ResourcesPreviewGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ResourcesPreviewGetRequestV2( resourceIris = resourceIris, @@ -423,7 +420,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -451,7 +448,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ResourceTEIGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ResourceTEIGetRequestV2( resourceIri = resourceIri, @@ -480,14 +477,14 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) throw BadRequestException(s"Invalid resource IRI: <$resIriStr>") ) val params: Map[String, String] = requestContext.request.uri.query().toMap - val depth: Int = params.get(Depth).map(_.toInt).getOrElse(appConfig.v2.graphRoute.defaultGraphDepth) + val depth: Int = params.get(Depth).map(_.toInt).getOrElse(routeData.appConfig.v2.graphRoute.defaultGraphDepth) if (depth < 1) { throw BadRequestException(s"$Depth must be at least 1") } - if (depth > appConfig.v2.graphRoute.maxGraphDepth) { - throw BadRequestException(s"$Depth cannot be greater than ${appConfig.v2.graphRoute.maxGraphDepth}") + if (depth > routeData.appConfig.v2.graphRoute.maxGraphDepth) { + throw BadRequestException(s"$Depth cannot be greater than ${routeData.appConfig.v2.graphRoute.maxGraphDepth}") } val direction: String = params.getOrElse(Direction, Outbound) @@ -507,7 +504,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[GraphDataGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield GraphDataGetRequestV2( resourceIri = resourceIri, @@ -521,7 +518,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -539,7 +536,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteOrEraseResourceRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: DeleteOrEraseResourceRequestV2 <- DeleteOrEraseResourceRequestV2.fromJsonLD( requestDoc, @@ -553,7 +550,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -573,7 +570,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteOrEraseResourceRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: DeleteOrEraseResourceRequestV2 <- DeleteOrEraseResourceRequestV2.fromJsonLD( @@ -588,7 +585,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -702,33 +699,33 @@ class ResourcesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) value.valueContent match { case fileValueContent: StillImageFileValueContentV2 => { println("StillImageFileValueContentV2") - if (!appConfig.sipi.imageMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + if (!routeData.appConfig.sipi.imageMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } } case fileValueContent: DocumentFileValueContentV2 => { - if (!appConfig.sipi.documentMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + if (!routeData.appConfig.sipi.documentMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } } case fileValueContent: ArchiveFileValueContentV2 => { - if (!appConfig.sipi.archiveMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + if (!routeData.appConfig.sipi.archiveMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } } case fileValueContent: TextFileValueContentV2 => { println("TextFileValueContentV2") - if (!appConfig.sipi.textMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + if (!routeData.appConfig.sipi.textMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } } case fileValueContent: AudioFileValueContentV2 => { - if (!appConfig.sipi.audioMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + if (!routeData.appConfig.sipi.audioMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } } case fileValueContent: MovingImageFileValueContentV2 => { - if (!appConfig.sipi.videoMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { + if (!routeData.appConfig.sipi.videoMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala index 366938ce72..1d759b3d6b 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala @@ -13,7 +13,6 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -28,9 +27,7 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a function for API routes that deal with search. */ -class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { private val LIMIT_TO_PROJECT = "limitToProject" private val LIMIT_TO_RESOURCE_CLASS = "limitToResourceClass" @@ -172,9 +169,9 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) throw BadRequestException(s"Invalid search string: '$searchStr'") ) - if (escapedSearchStr.length < appConfig.v2.fulltextSearch.searchValueMinLength) { + if (escapedSearchStr.length < routeData.appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." + s"A search value is expected to have at least length of ${routeData.appConfig.v2.fulltextSearch.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." ) } @@ -187,7 +184,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val limitToStandoffClass: Option[SmartIri] = getStandoffClass(params) val requestMessage: Future[FullTextSearchCountRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield FullTextSearchCountRequestV2( searchValue = escapedSearchStr, limitToProject = limitToProject, @@ -199,7 +196,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -223,9 +220,9 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) throw BadRequestException(s"Invalid search string: '$searchStr'") ) - if (escapedSearchStr.length < appConfig.v2.fulltextSearch.searchValueMinLength) { + if (escapedSearchStr.length < routeData.appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." + s"A search value is expected to have at least length of ${routeData.appConfig.v2.fulltextSearch.searchValueMinLength}, but '$escapedSearchStr' given of length ${escapedSearchStr.length}." ) } @@ -248,7 +245,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val schemaOptions: Set[SchemaOption] = RouteUtilV2.getSchemaOptions(requestContext) val requestMessage: Future[FulltextSearchRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield FulltextSearchRequestV2( searchValue = escapedSearchStr, offset = offset, @@ -264,7 +261,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -280,7 +277,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val constructQuery = GravsearchParser.parseQuery(gravsearchQuery) val requestMessage: Future[GravsearchCountRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GravsearchCountRequestV2( constructQuery = constructQuery, requestingUser = requestingUser @@ -289,7 +286,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -305,7 +302,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) { val constructQuery = GravsearchParser.parseQuery(gravsearchQuery) val requestMessage: Future[GravsearchCountRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GravsearchCountRequestV2( constructQuery = constructQuery, requestingUser = requestingUser @@ -314,7 +311,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -334,7 +331,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val schemaOptions: Set[SchemaOption] = RouteUtilV2.getSchemaOptions(requestContext) val requestMessage: Future[GravsearchRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GravsearchRequestV2( constructQuery = constructQuery, targetSchema = targetSchema, @@ -345,7 +342,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -363,7 +360,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val schemaOptions: Set[SchemaOption] = RouteUtilV2.getSchemaOptions(requestContext) val requestMessage: Future[GravsearchRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield GravsearchRequestV2( constructQuery = constructQuery, targetSchema = targetSchema, @@ -374,7 +371,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -395,9 +392,9 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) throw BadRequestException(s"Invalid search string: '$searchval'") ) - if (searchString.length < appConfig.v2.fulltextSearch.searchValueMinLength) { + if (searchString.length < routeData.appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." + s"A search value is expected to have at least length of ${routeData.appConfig.v2.fulltextSearch.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." ) } @@ -408,7 +405,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val limitToResourceClass: Option[SmartIri] = getResourceClassFromParams(params) val requestMessage: Future[SearchResourceByLabelCountRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield SearchResourceByLabelCountRequestV2( searchValue = searchString, limitToProject = limitToProject, @@ -419,7 +416,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), @@ -438,9 +435,9 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) throw BadRequestException(s"Invalid search string: '$searchval'") ) - if (searchString.length < appConfig.v2.fulltextSearch.searchValueMinLength) { + if (searchString.length < routeData.appConfig.v2.fulltextSearch.searchValueMinLength) { throw BadRequestException( - s"A search value is expected to have at least length of ${appConfig.v2.fulltextSearch.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." + s"A search value is expected to have at least length of ${routeData.appConfig.v2.fulltextSearch.searchValueMinLength}, but '$searchString' given of length ${searchString.length}." ) } @@ -455,7 +452,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val targetSchema: ApiV2Schema = RouteUtilV2.getOntologySchema(requestContext) val requestMessage: Future[SearchResourceByLabelRequestV2] = for { - requestingUser <- getUserADM(requestContext, appConfig) + requestingUser <- getUserADM(requestContext, routeData.appConfig) } yield SearchResourceByLabelRequestV2( searchValue = searchString, offset = offset, @@ -468,7 +465,7 @@ class SearchRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = RouteUtilV2.getOntologySchema(requestContext), diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala index f8a4a8477f..c433cef067 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/StandoffRouteV2.scala @@ -16,7 +16,6 @@ import scala.concurrent.duration._ import dsp.errors.BadRequestException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.rdf.JsonLDUtil @@ -32,9 +31,7 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a function for API routes that deal with search. */ -class StandoffRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { /** * Returns the route. @@ -67,7 +64,7 @@ class StandoffRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[GetStandoffPageRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield GetStandoffPageRequestV2( resourceIri = resourceIri.toString, @@ -80,7 +77,7 @@ class StandoffRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -126,7 +123,7 @@ class StandoffRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CreateMappingRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) allParts: Map[Name, String] <- allPartsFuture jsonldDoc = @@ -164,7 +161,7 @@ class StandoffRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala index 6efa9f981e..d8b24fe559 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala @@ -15,7 +15,6 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.rdf.JsonLDDocument @@ -30,9 +29,7 @@ import org.knora.webapi.routing.RouteUtilV2 /** * Provides a routing function for API v2 routes that deal with values. */ -class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) - extends KnoraRoute(routeData, appConfig) - with Authenticator { +class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { val valuesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "values") @@ -82,7 +79,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[ResourcesGetRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) } yield ResourcesGetRequestV2( resourceIris = Seq(resourceIri.toString), @@ -95,7 +92,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = targetSchema, @@ -113,7 +110,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[CreateValueRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: CreateValueRequestV2 <- CreateValueRequestV2.fromJsonLD( requestDoc, @@ -127,7 +124,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -147,7 +144,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[UpdateValueRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: UpdateValueRequestV2 <- UpdateValueRequestV2.fromJsonLD( requestDoc, @@ -161,7 +158,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, @@ -181,7 +178,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) val requestMessageFuture: Future[DeleteValueRequestV2] = for { requestingUser <- getUserADM( requestContext = requestContext, - appConfig + routeData.appConfig ) requestMessage: DeleteValueRequestV2 <- DeleteValueRequestV2.fromJsonLD( requestDoc, @@ -195,7 +192,7 @@ class ValuesRouteV2(routeData: KnoraRouteData, appConfig: AppConfig) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, - appConfig = appConfig, + appConfig = routeData.appConfig, appActor = appActor, log = log, targetSchema = ApiV2Complex, diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala index 868b840785..2f5d0b8408 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ResourcesV1R2RSpec.scala @@ -56,11 +56,11 @@ import org.knora.webapi.util.MutableTestIri class ResourcesV1R2RSpec extends R2RSpec { private val resourcesPathV1 = - DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV1(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV1(routeData).makeRoute) private val resourcesPathV2 = - DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData).makeRoute) private val valuesPathV1 = - DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData).makeRoute) private val superUser = SharedTestDataADM.superUser private val superUserEmail = superUser.email diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala index 9238007ef2..719d050ec9 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SearchV1R2RSpec.scala @@ -24,7 +24,7 @@ import org.knora.webapi.routing.v1.SearchRouteV1 */ class SearchV1R2RSpec extends R2RSpec { - private val searchPath = new SearchRouteV1(routeData, appConfig).makeRoute + private val searchPath = new SearchRouteV1(routeData).makeRoute implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala index 0f2a43fa1c..115802131c 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/SipiV1R2RSpec.scala @@ -31,8 +31,8 @@ import org.knora.webapi.sharedtestdata.SharedTestDataV1 */ class SipiV1R2RSpec extends R2RSpec { - private val resourcesPath = new ResourcesRouteV1(routeData, appConfig).makeRoute - private val valuesPath = new ValuesRouteV1(routeData, appConfig).makeRoute + private val resourcesPath = new ResourcesRouteV1(routeData).makeRoute + private val valuesPath = new ValuesRouteV1(routeData).makeRoute implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala index 93328e1e82..baa1834fcb 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/StandoffV1R2RSpec.scala @@ -43,9 +43,9 @@ import org.knora.webapi.util.MutableTestIri class StandoffV1R2RSpec extends R2RSpec { private val standoffPath = - DSPApiDirectives.handleErrors(system, appConfig)(new StandoffRouteV1(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new StandoffRouteV1(routeData).makeRoute) private val valuesPath = - DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData).makeRoute) private val anythingUser = SharedTestDataV1.anythingUser1 private val anythingUserEmail = anythingUser.userData.email.get diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala index dd6f05ddf4..47b34d81f9 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v1/ValuesV1R2RSpec.scala @@ -30,7 +30,7 @@ import org.knora.webapi.util.MutableTestIri class ValuesV1R2RSpec extends R2RSpec { private val valuesPath = - DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData).makeRoute) implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala index 97613dc4ae..57f71bade1 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/JSONLDHandlingV2R2RSpec.scala @@ -25,7 +25,7 @@ import org.knora.webapi.routing.v2.ResourcesRouteV2 */ class JSONLDHandlingV2R2RSpec extends R2RSpec { - private val resourcesPath = new ResourcesRouteV2(routeData, appConfig).makeRoute + private val resourcesPath = new ResourcesRouteV2(routeData).makeRoute implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala index e1a79b4a45..09bc9ca9d4 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ListsRouteV2R2RSpec.scala @@ -31,7 +31,7 @@ import org.knora.webapi.util.FileUtil */ class ListsRouteV2R2RSpec extends R2RSpec { - private val listsPath = new ListsRouteV2(routeData, appConfig).makeRoute + private val listsPath = new ListsRouteV2(routeData).makeRoute implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala index a4c71e5802..0c4797d211 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/OntologyV2R2RSpec.scala @@ -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, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData).makeRoute) private val resourcesPath = - DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData).makeRoute) implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 9f572fd09f..3a8a02de3b 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -2108,7 +2108,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, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new OntologiesRouteV2(routeData).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/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index 3064d1f837..525df34688 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -52,13 +52,13 @@ class SearchRouteV2R2RSpec extends R2RSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance private val searchPath = - DSPApiDirectives.handleErrors(system, appConfig)(new SearchRouteV2(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new SearchRouteV2(routeData).makeRoute) private val resourcePath = - DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ResourcesRouteV2(routeData).makeRoute) private val standoffPath = - DSPApiDirectives.handleErrors(system, appConfig)(new StandoffRouteV2(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new StandoffRouteV2(routeData).makeRoute) private val valuesPath = - DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData, appConfig).makeRoute) + DSPApiDirectives.handleErrors(system, appConfig)(new ValuesRouteV1(routeData).makeRoute) implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala index 110b705e78..a145daa5bb 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesV2R2RSpec.scala @@ -37,8 +37,8 @@ class ValuesV2R2RSpec extends R2RSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val valuesPath = new ValuesRouteV2(routeData, appConfig).makeRoute - private val searchPath = new SearchRouteV2(routeData, appConfig).makeRoute + private val valuesPath = new ValuesRouteV2(routeData).makeRoute + private val searchPath = new SearchRouteV2(routeData).makeRoute implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout( appConfig.defaultTimeoutAsDuration From 56f99a1bc599e31eaf4ef718a0b70358b23f99e3 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 14:28:07 +0200 Subject: [PATCH 31/44] remove appConfig as param from responders --- .../webapi/core/actors/RoutingActor.scala | 46 +++++----- .../webapi/messages/util/ResponderData.scala | 2 + ...tationReadingGravsearchTypeInspector.scala | 6 +- .../GravsearchTypeInspectionRunner.scala | 10 +-- .../types/GravsearchTypeInspector.scala | 6 +- .../InferringGravsearchTypeInspector.scala | 6 +- .../knora/webapi/responders/Responder.scala | 5 +- .../responders/admin/GroupsResponderADM.scala | 5 +- .../responders/admin/ListsResponderADM.scala | 6 +- .../admin/PermissionsResponderADM.scala | 4 +- .../admin/ProjectsResponderADM.scala | 5 +- .../responders/admin/SipiResponderADM.scala | 3 +- .../responders/admin/StoresResponderADM.scala | 6 +- .../responders/admin/UsersResponderADM.scala | 9 +- .../responders/v1/CkanResponderV1.scala | 3 +- .../responders/v1/ListsResponderV1.scala | 7 +- .../responders/v1/OntologyResponderV1.scala | 26 +++--- .../responders/v1/ProjectsResponderV1.scala | 4 +- .../responders/v1/ResourcesResponderV1.scala | 40 +++++---- .../responders/v1/SearchResponderV1.scala | 32 +++---- .../responders/v1/StandoffResponderV1.scala | 4 +- .../responders/v1/UsersResponderV1.scala | 9 +- .../responders/v1/ValuesResponderV1.scala | 6 +- .../responders/v2/ListsResponderV2.scala | 15 +++- .../responders/v2/OntologyResponderV2.scala | 6 +- .../responders/v2/ResourcesResponderV2.scala | 25 +++--- .../v2/ResponderWithStandoffV2.scala | 4 +- .../responders/v2/SearchResponderV2.scala | 28 +++--- .../responders/v2/StandoffResponderV2.scala | 12 ++- .../responders/v2/ValuesResponderV2.scala | 4 +- .../scala/org/knora/webapi/CoreSpec.scala | 2 +- ...searchToCountPrequeryTransformerSpec.scala | 3 +- ...cGravsearchToPrequeryTransformerSpec.scala | 3 +- .../types/GravsearchTypeInspectorSpec.scala | 87 +++++++------------ .../admin/PermissionsResponderADMSpec.scala | 2 +- .../webapi/util/cache/CacheUtilSpec.scala | 7 +- 36 files changed, 191 insertions(+), 257 deletions(-) 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 0a5616e8f4..75ebd3dc8c 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 @@ -91,35 +91,35 @@ class RoutingActor( /** * Data used in responders. */ - val responderData: ResponderData = ResponderData(system, self, cacheServiceSettings) + val responderData: ResponderData = ResponderData(system, self, appConfig, cacheServiceSettings) // V1 responders - val ckanResponderV1: CkanResponderV1 = new CkanResponderV1(responderData, appConfig) - val resourcesResponderV1: ResourcesResponderV1 = new ResourcesResponderV1(responderData, appConfig) - val valuesResponderV1: ValuesResponderV1 = new ValuesResponderV1(responderData, appConfig) - val standoffResponderV1: StandoffResponderV1 = new StandoffResponderV1(responderData, appConfig) - val usersResponderV1: UsersResponderV1 = new UsersResponderV1(responderData, appConfig) - val listsResponderV1: ListsResponderV1 = new ListsResponderV1(responderData, appConfig) - val searchResponderV1: SearchResponderV1 = new SearchResponderV1(responderData, appConfig) - val ontologyResponderV1: OntologyResponderV1 = new OntologyResponderV1(responderData, appConfig) - val projectsResponderV1: ProjectsResponderV1 = new ProjectsResponderV1(responderData, appConfig) + 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) // V2 responders - val ontologiesResponderV2: OntologyResponderV2 = new OntologyResponderV2(responderData, appConfig) - val searchResponderV2: SearchResponderV2 = new SearchResponderV2(responderData, appConfig) - val resourcesResponderV2: ResourcesResponderV2 = new ResourcesResponderV2(responderData, appConfig) - val valuesResponderV2: ValuesResponderV2 = new ValuesResponderV2(responderData, appConfig) - val standoffResponderV2: StandoffResponderV2 = new StandoffResponderV2(responderData, appConfig) - val listsResponderV2: ListsResponderV2 = new ListsResponderV2(responderData, appConfig) + 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) // Admin responders - val groupsResponderADM: GroupsResponderADM = new GroupsResponderADM(responderData, appConfig) - val listsResponderADM: ListsResponderADM = new ListsResponderADM(responderData, appConfig) - val permissionsResponderADM: PermissionsResponderADM = new PermissionsResponderADM(responderData, appConfig) - val projectsResponderADM: ProjectsResponderADM = new ProjectsResponderADM(responderData, appConfig) - val storeResponderADM: StoresResponderADM = new StoresResponderADM(responderData, appConfig) - val usersResponderADM: UsersResponderADM = new UsersResponderADM(responderData, appConfig) - val sipiRouterADM: SipiResponderADM = new SipiResponderADM(responderData, appConfig) + 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) def receive: Receive = { 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 527796282f..de158cec61 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 @@ -8,6 +8,7 @@ package org.knora.webapi.messages.util import akka.actor.ActorRef import akka.actor.ActorSystem +import org.knora.webapi.config.AppConfig import org.knora.webapi.store.cache.settings.CacheServiceSettings /** @@ -20,5 +21,6 @@ import org.knora.webapi.store.cache.settings.CacheServiceSettings case class ResponderData( system: ActorSystem, appActor: ActorRef, + appConfig: AppConfig, cacheServiceSettings: CacheServiceSettings ) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala index 888ef0805a..a73a6d4f8d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/AnnotationReadingGravsearchTypeInspector.scala @@ -10,7 +10,6 @@ import scala.concurrent.Future import dsp.errors.AssertionException import dsp.errors.GravsearchException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -31,9 +30,8 @@ import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInsp */ class AnnotationReadingGravsearchTypeInspector( nextInspector: Option[GravsearchTypeInspector], - responderData: ResponderData, - appConfig: AppConfig -) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData, appConfig = appConfig) { + responderData: ResponderData +) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData) { /** * Represents a Gravsearch type annotation. 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 9786d41228..5254302468 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 @@ -11,7 +11,6 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import dsp.errors.GravsearchException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -30,8 +29,7 @@ import org.knora.webapi.settings.KnoraDispatchers class GravsearchTypeInspectionRunner( appActor: ActorRef, responderData: ResponderData, - inferTypes: Boolean = true, - appConfig: AppConfig + inferTypes: Boolean = true ) { private implicit val executionContext: ExecutionContext = responderData.system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) @@ -42,8 +40,7 @@ class GravsearchTypeInspectionRunner( new InferringGravsearchTypeInspector( nextInspector = None, appActor = appActor, - responderData = responderData, - appConfig = appConfig + responderData = responderData ) ) } else { @@ -53,8 +50,7 @@ class GravsearchTypeInspectionRunner( // The pipeline of type inspectors. private val typeInspectionPipeline = new AnnotationReadingGravsearchTypeInspector( nextInspector = maybeInferringTypeInspector, - responderData = responderData, - appConfig = appConfig + responderData = responderData ) /** 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 c12a0d6b62..dcf8a6bab8 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 @@ -11,7 +11,6 @@ import akka.util.Timeout import scala.concurrent.ExecutionContext import scala.concurrent.Future -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.search.WhereClause @@ -27,14 +26,13 @@ import org.knora.webapi.settings.KnoraDispatchers */ abstract class GravsearchTypeInspector( protected val nextInspector: Option[GravsearchTypeInspector], - responderData: ResponderData, - appConfig: AppConfig + responderData: ResponderData ) { protected val system: ActorSystem = responderData.system protected implicit val executionContext: ExecutionContext = system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) - protected implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration + protected implicit val timeout: Timeout = responderData.appConfig.defaultTimeoutAsDuration /** * 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/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala index a5fd9c8cd7..695a35764a 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/InferringGravsearchTypeInspector.scala @@ -16,7 +16,6 @@ import scala.concurrent.Future import dsp.errors.AssertionException import dsp.errors.GravsearchException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -36,9 +35,8 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV class InferringGravsearchTypeInspector( nextInspector: Option[GravsearchTypeInspector], appActor: ActorRef, - responderData: ResponderData, - appConfig: AppConfig -) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData, appConfig = appConfig) { + responderData: ResponderData +) extends GravsearchTypeInspector(nextInspector = nextInspector, responderData = responderData) { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance 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 3dd82c9178..fd534621e6 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/Responder.scala @@ -18,7 +18,6 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import dsp.errors._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.settings.KnoraDispatchers import org.knora.webapi.store.cache.settings.CacheServiceSettings @@ -50,7 +49,7 @@ object Responder { /** * An abstract class providing values that are commonly used in Knora responders. */ -abstract class Responder(responderData: ResponderData, appConfig: AppConfig) extends LazyLogging { +abstract class Responder(responderData: ResponderData) extends LazyLogging { /** * The actor system. @@ -81,7 +80,7 @@ abstract class Responder(responderData: ResponderData, appConfig: AppConfig) ext /** * The application's default timeout for `ask` messages. */ - protected implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration + protected implicit val timeout: Timeout = responderData.appConfig.defaultTimeoutAsDuration /** * Provides logging 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 ecee4e5b4b..6bc3d99b45 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 @@ -14,7 +14,6 @@ import scala.concurrent.Future import dsp.errors._ import dsp.valueobjects.Group.GroupStatus import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -34,9 +33,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Returns information about Knora projects. */ -class GroupsResponderADM(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) - with GroupsADMJsonProtocol { +class GroupsResponderADM(responderData: ResponderData) extends Responder(responderData) with GroupsADMJsonProtocol { // Global lock IRI used for group creation and updating private val GROUPS_GLOBAL_LOCK_IRI: IRI = "http://rdfh.ch/groups" 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 e512420691..0d82c5b768 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 @@ -17,7 +17,6 @@ import dsp.valueobjects.Iri._ import dsp.valueobjects.List.ListName import dsp.valueobjects.ListErrorMessages import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -39,8 +38,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * A responder that returns information about hierarchical lists. */ -class ListsResponderADM(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class ListsResponderADM(responderData: ResponderData) extends Responder(responderData) { // The IRI used to lock user creation and update private val LISTS_GLOBAL_LOCK_IRI = "http://rdfh.ch/lists" @@ -800,7 +798,7 @@ class ListsResponderADM(responderData: ResponderData, appConfig: AppConfig) .getNodePath( queryNodeIri = queryNodeIri, preferredLanguage = requestingUser.lang, - fallbackLanguage = appConfig.fallbackLanguage + fallbackLanguage = responderData.appConfig.fallbackLanguage ) .toString() } 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 80d109fcad..674b47fe61 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 @@ -15,7 +15,6 @@ import scala.concurrent.Future import dsp.errors._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -41,8 +40,7 @@ import org.knora.webapi.util.cache.CacheUtil /** * Provides information about permissions to other responders. */ -class PermissionsResponderADM(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class PermissionsResponderADM(responderData: ResponderData) extends Responder(responderData) { private val PERMISSIONS_GLOBAL_LOCK_IRI = "http://rdfh.ch/permissions" /* Entity types used to more clearly distinguish what kind of entity is meant */ 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 8c7a67bb06..718c54afa1 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 @@ -20,7 +20,6 @@ import scala.util.Try import dsp.errors._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.instrumentation.InstrumentationSupport import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants @@ -47,9 +46,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Returns information about Knora projects. */ -class ProjectsResponderADM(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) - with InstrumentationSupport { +class ProjectsResponderADM(responderData: ResponderData) extends Responder(responderData) with InstrumentationSupport { // Global lock IRI used for project creation and update private val PROJECTS_GLOBAL_LOCK_IRI = "http://rdfh.ch/projects" 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 40e8a19e51..c4169db62e 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 @@ -12,7 +12,6 @@ import scala.concurrent.Future import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectRestrictedViewSettingsADM @@ -35,7 +34,7 @@ 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, appConfig: AppConfig) extends Responder(responderData, appConfig) { +class SipiResponderADM(responderData: ResponderData) extends Responder(responderData) { /** * 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 7951764dde..d657292e91 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 @@ -11,7 +11,6 @@ import akka.pattern._ import scala.concurrent.Future import dsp.errors.ForbiddenException -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.storesmessages.ResetTriplestoreContentRequestADM import org.knora.webapi.messages.admin.responder.storesmessages.ResetTriplestoreContentResponseADM import org.knora.webapi.messages.admin.responder.storesmessages.StoreResponderRequestADM @@ -30,8 +29,7 @@ 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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class StoresResponderADM(responderData: ResponderData) extends Responder(responderData) { /** * A user representing the Knora API server, used in those cases where a user is required. @@ -65,7 +63,7 @@ class StoresResponderADM(responderData: ResponderData, appConfig: AppConfig) for { // FIXME: need to call directly into the State service - value: Boolean <- FastFuture.successful(appConfig.allowReloadOverHttp) + value: Boolean <- FastFuture.successful(responderData.appConfig.allowReloadOverHttp) _ = if (!value) { throw ForbiddenException( "The ResetTriplestoreContent operation is not allowed. Did you start the server with the right flag?" 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 64e6cfd6c7..5b7070cc73 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 @@ -17,7 +17,6 @@ import dsp.errors.InconsistentRepositoryDataException import dsp.errors._ import dsp.valueobjects.User._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.instrumentation.InstrumentationSupport import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants @@ -45,9 +44,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Provides information about Knora users to other responders. */ -class UsersResponderADM(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) - with InstrumentationSupport { +class UsersResponderADM(responderData: ResponderData) extends Responder(responderData) with InstrumentationSupport { // The IRI used to lock user creation and update private val USERS_GLOBAL_LOCK_IRI = "http://rdfh.ch/users" @@ -502,7 +499,7 @@ class UsersResponderADM(responderData: ResponderData, appConfig: AppConfig) } // hash the new password - encoder = new BCryptPasswordEncoder(appConfig.bcryptPasswordStrength) + encoder = new BCryptPasswordEncoder(responderData.appConfig.bcryptPasswordStrength) newHashedPassword = Password .make(encoder.encode(userUpdatePasswordPayload.newPassword.value)) .fold(e => throw e.head, value => value) @@ -1676,7 +1673,7 @@ class UsersResponderADM(responderData: ResponderData, appConfig: AppConfig) userIri: IRI <- checkOrCreateEntityIri(customUserIri, stringFormatter.makeRandomPersonIri) // hash password - encoder = new BCryptPasswordEncoder(appConfig.bcryptPasswordStrength) + encoder = new BCryptPasswordEncoder(responderData.appConfig.bcryptPasswordStrength) hashedPassword = encoder.encode(userCreatePayloadADM.password.value) // Create the new user. 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 9555cca379..8bfcd67de4 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 @@ -15,7 +15,6 @@ import scala.concurrent.Future import scala.concurrent.duration._ import org.knora.webapi.IRI -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages.SparqlSelectRequest @@ -41,7 +40,7 @@ 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, appConfig: AppConfig) extends Responder(responderData, appConfig) { +class CkanResponderV1(responderData: ResponderData) extends Responder(responderData) { /** * 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 e00aace2dc..0d5f0c9598 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 @@ -12,7 +12,6 @@ import scala.concurrent.Future import dsp.errors.NotFoundException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.store.triplestoremessages.SparqlSelectRequest import org.knora.webapi.messages.util.ResponderData import org.knora.webapi.messages.util.rdf.SparqlSelectResult @@ -25,7 +24,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * A responder that returns information about hierarchical lists. */ -class ListsResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData, appConfig) { +class ListsResponderV1(responderData: ResponderData) extends Responder(responderData) { /** * Receives a message of type [[ListsResponderRequestV1]], and returns an appropriate response message. @@ -148,7 +147,7 @@ class ListsResponderV1(responderData: ResponderData, appConfig: AppConfig) exten .getList( rootNodeIri = rootNodeIri, preferredLanguage = userProfile.userData.lang, - fallbackLanguage = appConfig.fallbackLanguage + fallbackLanguage = responderData.appConfig.fallbackLanguage ) .toString() } @@ -230,7 +229,7 @@ class ListsResponderV1(responderData: ResponderData, appConfig: AppConfig) exten .getNodePath( queryNodeIri = queryNodeIri, preferredLanguage = userProfile.userData.lang, - fallbackLanguage = appConfig.fallbackLanguage + fallbackLanguage = responderData.appConfig.fallbackLanguage ) .toString() } 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 bcf4058072..5bd5c6be93 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 @@ -13,7 +13,6 @@ import dsp.constants.SalsahGui import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM @@ -36,10 +35,9 @@ 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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class OntologyResponderV1(responderData: ResponderData) extends Responder(responderData) { - private val valueUtilV1 = new ValueUtilV1(appConfig) + private val valueUtilV1 = new ValueUtilV1(responderData.appConfig) /** * Receives a message extending [[OntologyResponderRequestV1]], and returns an appropriate response message. @@ -242,14 +240,14 @@ class OntologyResponderV1(responderData: ResponderData, appConfig: AppConfig) predicateIri = OntologyConstants.Rdfs.Label, preferredLangs = Some( userProfile.lang, - appConfig.fallbackLanguage + responderData.appConfig.fallbackLanguage ) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, preferredLangs = Some( userProfile.lang, - appConfig.fallbackLanguage + responderData.appConfig.fallbackLanguage ) ), vocabulary = entityInfo.ontologyIri, @@ -292,14 +290,14 @@ class OntologyResponderV1(responderData: ResponderData, appConfig: AppConfig) predicateIri = OntologyConstants.Rdfs.Label, preferredLangs = Some( userProfile.lang, - appConfig.fallbackLanguage + responderData.appConfig.fallbackLanguage ) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, preferredLangs = Some( userProfile.lang, - appConfig.fallbackLanguage + responderData.appConfig.fallbackLanguage ) ), vocabulary = entityInfo.ontologyIri, @@ -342,11 +340,11 @@ class OntologyResponderV1(responderData: ResponderData, appConfig: AppConfig) name = resourceTypeIri, label = resourceClassInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), description = resourceClassInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), iconsrc = resourceClassInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon), @@ -622,11 +620,11 @@ class OntologyResponderV1(responderData: ResponderData, appConfig: AppConfig) name = propertyIri, label = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), vocabulary = entityInfo.ontologyIri, valuetype_id = OntologyConstants.KnoraBase.LinkValue, @@ -654,11 +652,11 @@ class OntologyResponderV1(responderData: ResponderData, appConfig: AppConfig) name = propertyIri, label = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), vocabulary = entityInfo.ontologyIri, valuetype_id = entityInfo 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 c6101f0f1e..e4a1650f1f 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 @@ -13,7 +13,6 @@ import scala.concurrent.Future import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.admin.responder.usersmessages.UserGetRequestADM @@ -35,8 +34,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Returns information about Knora projects. */ -class ProjectsResponderV1(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class ProjectsResponderV1(responderData: ResponderData) extends Responder(responderData) { // 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 2cd872d672..5583b3bb39 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 @@ -19,7 +19,6 @@ import dsp.constants.SalsahGui import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -59,11 +58,10 @@ 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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class ResourcesResponderV1(responderData: ResponderData) extends Responder(responderData) { // Converts SPARQL query results to ApiValueV1 objects. - private val valueUtilV1 = new ValueUtilV1(appConfig) + private val valueUtilV1 = new ValueUtilV1(responderData.appConfig) /** * Receives a message extending [[ResourcesResponderRequestV1]], and returns an appropriate response message. @@ -437,8 +435,10 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) .resourceClassInfoMap(node.nodeClass) .getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = - Some(graphDataGetRequest.userADM.lang, appConfig.fallbackLanguage) + preferredLangs = Some( + graphDataGetRequest.userADM.lang, + responderData.appConfig.fallbackLanguage + ) ) .getOrElse( throw InconsistentRepositoryDataException( @@ -461,8 +461,10 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) .propertyInfoMap(edge.linkProp) .getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = - Some(graphDataGetRequest.userADM.lang, appConfig.fallbackLanguage) + preferredLangs = Some( + graphDataGetRequest.userADM.lang, + responderData.appConfig.fallbackLanguage + ) ) .getOrElse( throw InconsistentRepositoryDataException( @@ -768,14 +770,14 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) resourceClassLabel = resourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ) resInfo: ResourceInfoV1 = resInfoWithoutQueryingOntology.copy( restype_label = resourceClassLabel, restype_description = resourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), restype_iconsrc = maybeResourceTypeIconSrc ) @@ -798,11 +800,11 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) resinfo = incoming.resinfo.copy( restype_label = incomingResourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), restype_description = incomingResourceTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), restype_iconsrc = incomingResourceTypeEntityInfo.getPredicateObject( OntologyConstants.KnoraBase.ResourceIcon @@ -874,7 +876,8 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) ), label = propertyEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = + Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), occurrence = Some(propsAndCardinalities(propertyIri).cardinality.value), attributes = (propertyEntityInfo.getPredicateStringObjectsWithoutLang( @@ -905,7 +908,8 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) ), label = propertyEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = + Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ), occurrence = Some(propsAndCardinalities(propertyIri).cardinality.value), attributes = propertyEntityInfo @@ -3098,11 +3102,11 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) entityInfo = entityInfoResponse.resourceClassInfoMap(resTypeIri) label = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ) description = entityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Comment, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ) iconsrc = entityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) match { case Some(resClassIcon) => @@ -3229,7 +3233,7 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) label = propertyEntityInfo.flatMap( _.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ) ), occurrence = propertyCardinality.map(_.cardinality.value), @@ -3377,7 +3381,7 @@ class ResourcesResponderV1(responderData: ResponderData, appConfig: AppConfig) case Some(referencedResTypeEntityInfo) => val labelOption: Option[String] = referencedResTypeEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(userProfile.lang, responderData.appConfig.fallbackLanguage) ) val resIconOption: Option[String] = referencedResTypeEntityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) 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 a082d91c14..1bd3607d4f 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 @@ -12,7 +12,6 @@ import scala.concurrent.Future import dsp.errors.BadRequestException import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.store.triplestoremessages.SparqlSelectRequest @@ -36,8 +35,7 @@ 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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class SearchResponderV1(responderData: ResponderData) extends Responder(responderData) { // 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 @@ -135,7 +133,7 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) valuePermissionCode: Option[Int] ) - val valueUtilV1 = new ValueUtilV1(appConfig) + val valueUtilV1 = new ValueUtilV1(responderData.appConfig) /** * Receives a message of type [[SearchResponderRequestV1]], and returns an appropriate response message. @@ -168,7 +166,7 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) .searchFulltext( searchTerms = LuceneQueryString(searchGetRequest.searchValue), preferredLanguage = searchGetRequest.userProfile.lang, - fallbackLanguage = appConfig.fallbackLanguage, + fallbackLanguage = responderData.appConfig.fallbackLanguage, projectIriOption = searchGetRequest.filterByProject, restypeIriOption = searchGetRequest.filterByRestype ) @@ -230,7 +228,7 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) val resourceEntityInfo: ClassInfoV1 = entityInfoResponse.resourceClassInfoMap(resourceClassIri) val resourceClassLabel = resourceEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(searchGetRequest.userProfile.lang, responderData.appConfig.fallbackLanguage) ) val resourceClassIcon = resourceEntityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) val resourceLabel = firstRowMap.getOrElse( @@ -261,7 +259,8 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) .propertyInfoMap(propertyIri) .getPredicateObject( OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = + Some(searchGetRequest.userProfile.lang, responderData.appConfig.fallbackLanguage) ) match { case Some(label) => label case None => @@ -310,11 +309,11 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) value = resourceLabel +: matchingValues.map(_.literal), preview_nx = firstRowMap.get("previewDimX") match { case Some(previewDimX) => previewDimX.toInt - case None => appConfig.gui.defaultIconSize.dimX + case None => responderData.appConfig.gui.defaultIconSize.dimX }, preview_ny = firstRowMap.get("previewDimY") match { case Some(previewDimY) => previewDimY.toInt - case None => appConfig.gui.defaultIconSize.dimY + case None => responderData.appConfig.gui.defaultIconSize.dimY }, rights = resourcePermissionCode ) @@ -580,7 +579,7 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) .searchExtended( searchCriteria = searchCriteria, preferredLanguage = searchGetRequest.userProfile.lang, - fallbackLanguage = appConfig.fallbackLanguage, + fallbackLanguage = responderData.appConfig.fallbackLanguage, projectIriOption = searchGetRequest.filterByProject, restypeIriOption = searchGetRequest.filterByRestype, ownerIriOption = searchGetRequest.filterByOwner @@ -638,7 +637,7 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) val resourceEntityInfo = entityInfoResponse.resourceClassInfoMap(resourceClassIri) val resourceClassLabel = resourceEntityInfo.getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = Some(searchGetRequest.userProfile.lang, responderData.appConfig.fallbackLanguage) ) val resourceClassIcon = resourceEntityInfo.getPredicateObject(OntologyConstants.KnoraBase.ResourceIcon) val resourceLabel = firstRowMap.getOrElse( @@ -703,7 +702,8 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) .propertyInfoMap(propertyIri) .getPredicateObject( predicateIri = OntologyConstants.Rdfs.Label, - preferredLangs = Some(searchGetRequest.userProfile.lang, appConfig.fallbackLanguage) + preferredLangs = + Some(searchGetRequest.userProfile.lang, responderData.appConfig.fallbackLanguage) ) match { case Some(label) => label case None => @@ -761,11 +761,11 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) value = resourceLabel +: matchingValues.map(_.literal), preview_nx = firstRowMap.get("previewDimX") match { case Some(previewDimX) => previewDimX.toInt - case None => appConfig.gui.defaultIconSize.dimX + case None => responderData.appConfig.gui.defaultIconSize.dimX }, preview_ny = firstRowMap.get("previewDimY") match { case Some(previewDimY) => previewDimY.toInt - case None => appConfig.gui.defaultIconSize.dimY + case None => responderData.appConfig.gui.defaultIconSize.dimY }, rights = resourcePermissionCode ) @@ -832,7 +832,9 @@ class SearchResponderV1(responderData: ResponderData, appConfig: AppConfig) throw BadRequestException("Search limit must be greater than 0") } - if (limit > appConfig.maxResultsPerSearchResultPage) appConfig.maxResultsPerSearchResultPage else limit + if (limit > responderData.appConfig.maxResultsPerSearchResultPage) + responderData.appConfig.maxResultsPerSearchResultPage + else limit } /** 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 59868892e5..d73a7eb54e 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 @@ -12,7 +12,6 @@ import scala.concurrent.Future import dsp.errors.NotFoundException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -28,8 +27,7 @@ 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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class StandoffResponderV1(responderData: ResponderData) extends Responder(responderData) { /** * 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 e4c2e55666..32f71ed5d7 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 @@ -15,7 +15,6 @@ import dsp.errors.ApplicationCacheException import dsp.errors.ForbiddenException import dsp.errors.NotFoundException import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionDataGetADM import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsDataADM @@ -35,7 +34,7 @@ import org.knora.webapi.util.cache.CacheUtil /** * Provides information about Knora users to other responders. */ -class UsersResponderV1(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData, appConfig) { +class UsersResponderV1(responderData: ResponderData) extends Responder(responderData) { // The IRI used to lock user creation and update val USERS_GLOBAL_LOCK_IRI = "http://rdfh.ch/users" @@ -99,7 +98,7 @@ class UsersResponderV1(responderData: ResponderData, appConfig: AppConfig) exten UserDataV1( lang = propsMap.get(OntologyConstants.KnoraAdmin.PreferredLanguage) match { case Some(langList) => langList - case None => appConfig.fallbackLanguage + case None => responderData.appConfig.fallbackLanguage }, user_id = Some(userIri), email = propsMap.get(OntologyConstants.KnoraAdmin.Email), @@ -455,7 +454,7 @@ class UsersResponderV1(responderData: ResponderData, appConfig: AppConfig) exten val userDataV1 = UserDataV1( lang = groupedUserData.get(OntologyConstants.KnoraAdmin.PreferredLanguage) match { case Some(langList) => langList.head - case None => appConfig.fallbackLanguage + case None => responderData.appConfig.fallbackLanguage }, user_id = Some(returnedUserIri), email = groupedUserData.get(OntologyConstants.KnoraAdmin.Email).map(_.head), @@ -495,7 +494,7 @@ class UsersResponderV1(responderData: ResponderData, appConfig: AppConfig) exten val userDataV1 = UserDataV1( lang = groupedUserData.get(OntologyConstants.KnoraAdmin.PreferredLanguage) match { case Some(langList) => langList.head - case None => appConfig.fallbackLanguage + case None => responderData.appConfig.fallbackLanguage }, user_id = Some(returnedUserIri), email = groupedUserData.get(OntologyConstants.KnoraAdmin.Email).map(_.head), 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 0b7e8226f8..0686e914c8 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 @@ -14,7 +14,6 @@ import scala.concurrent.Future import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter @@ -55,10 +54,9 @@ import org.knora.webapi.util._ /** * Updates Knora values. */ -class ValuesResponderV1(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class ValuesResponderV1(responderData: ResponderData) extends Responder(responderData) { // Converts SPARQL query results to ApiValueV1 objects. - val valueUtilV1 = new ValueUtilV1(appConfig) + val valueUtilV1 = new ValueUtilV1(responderData.appConfig) /** * Receives a message of type [[ValuesResponderRequestV1]], and returns an appropriate response message. 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 f70872f1bf..217e685d8e 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 @@ -10,7 +10,6 @@ import akka.pattern._ import scala.concurrent.Future import org.knora.webapi.IRI -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.listsmessages.ChildNodeInfoGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ListGetRequestADM import org.knora.webapi.messages.admin.responder.listsmessages.ListGetResponseADM @@ -24,7 +23,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage /** * Responds to requests relating to lists and nodes. */ -class ListsResponderV2(responderData: ResponderData, appConfig: AppConfig) extends Responder(responderData, appConfig) { +class ListsResponderV2(responderData: ResponderData) extends Responder(responderData) { /** * Receives a message of type [[ListsResponderRequestV2]], and returns an appropriate response message inside a future. @@ -58,7 +57,11 @@ class ListsResponderV2(responderData: ResponderData, appConfig: AppConfig) exten ) .mapTo[ListGetResponseADM] - } yield ListGetResponseV2(list = listResponseADM.list, requestingUser.lang, appConfig.fallbackLanguage) + } yield ListGetResponseV2( + list = listResponseADM.list, + requestingUser.lang, + responderData.appConfig.fallbackLanguage + ) /** * Gets a single list node from the triplestore. @@ -81,6 +84,10 @@ class ListsResponderV2(responderData: ResponderData, appConfig: AppConfig) exten ) ) .mapTo[ChildNodeInfoGetResponseADM] - } yield NodeGetResponseV2(node = nodeResponse.nodeinfo, requestingUser.lang, appConfig.fallbackLanguage) + } yield NodeGetResponseV2( + node = nodeResponse.nodeinfo, + requestingUser.lang, + responderData.appConfig.fallbackLanguage + ) } 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 0f7a151c0d..a166b238f8 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 @@ -15,7 +15,6 @@ import dsp.constants.SalsahGui import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -61,8 +60,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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class OntologyResponderV2(responderData: ResponderData) extends Responder(responderData) { /** * Receives a message of type [[OntologiesResponderRequestV2]], and returns an appropriate response message. @@ -295,7 +293,7 @@ class OntologyResponderV2(responderData: ResponderData, appConfig: AppConfig) label = classInfo.entityInfoContent .getPredicateStringLiteralObject( predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, - preferredLangs = Some(requestingUser.lang, appConfig.fallbackLanguage) + preferredLangs = Some(requestingUser.lang, responderData.appConfig.fallbackLanguage) ) .getOrElse( throw InconsistentRepositoryDataException(s"Resource class $subClassIri has no rdfs:label") 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 47b6926d71..fa86ead457 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 @@ -17,7 +17,6 @@ import scala.util.Success import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -64,8 +63,7 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util._ -class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) - extends ResponderWithStandoffV2(responderData, appConfig) { +class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithStandoffV2(responderData) { /** * Represents a resource that is ready to be created and whose contents can be verified afterwards. @@ -1503,7 +1501,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) val (maybeStandoffMinStartIndex: Option[Int], maybeStandoffMaxStartIndex: Option[Int]) = StandoffTagUtilV2.getStandoffMinAndMaxStartIndexesForTextValueQuery( queryStandoff = queryStandoff, - appConfig = appConfig + appConfig = responderData.appConfig ) for { @@ -1609,7 +1607,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = targetSchema, - appConfig = appConfig, + appConfig = responderData.appConfig, requestingUser = requestingUser ) @@ -1700,7 +1698,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = false, appActor = appActor, targetSchema = targetSchema, - appConfig = appConfig, + appConfig = responderData.appConfig, requestingUser = requestingUser ) @@ -1790,7 +1788,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) } gravsearchUrl: String = - s"${appConfig.sipi.internalBaseUrl}/${resource.projectADM.shortcode}/${gravsearchFileValueContent.fileValue.internalFilename}/file" + s"${responderData.appConfig.sipi.internalBaseUrl}/${resource.projectADM.shortcode}/${gravsearchFileValueContent.fileValue.internalFilename}/file" } yield gravsearchUrl val recoveredGravsearchUrlFuture = gravsearchUrlFuture.recover { case notFound: NotFoundException => @@ -2074,7 +2072,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) header = TEIHeader( headerInfo = headerResource, headerXSLT = headerXSLT, - appConfig = appConfig + appConfig = responderData.appConfig ), body = TEIBody( bodyInfo = bodyTextValue, @@ -2173,7 +2171,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) startNodeOnly = false, maybeExcludeLinkProperty = excludePropertyInternal, outbound = outbound, // true to query outbound edges, false to query inbound edges - limit = appConfig.v2.graphRoute.maxGraphBreadth + limit = responderData.appConfig.v2.graphRoute.maxGraphBreadth ) .toString() ) @@ -2314,7 +2312,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) maybeExcludeLinkProperty = excludePropertyInternal, startNodeOnly = true, outbound = true, - limit = appConfig.v2.graphRoute.maxGraphBreadth + limit = responderData.appConfig.v2.graphRoute.maxGraphBreadth ) .toString() ) @@ -2563,7 +2561,10 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) } val fileUrl: String = - imageValueContent.makeFileUrl(projectADM = representation.projectADM, appConfig.sipi.externalBaseUrl) + imageValueContent.makeFileUrl( + projectADM = representation.projectADM, + responderData.appConfig.sipi.externalBaseUrl + ) JsonLDObject( Map( @@ -2604,7 +2605,7 @@ class ResourcesResponderV2(responderData: ResponderData, appConfig: AppConfig) Seq( JsonLDObject( Map( - "id" -> JsonLDString(appConfig.sipi.externalBaseUrl), + "id" -> JsonLDString(responderData.appConfig.sipi.externalBaseUrl), "type" -> JsonLDString("ImageService3"), "profile" -> JsonLDString("level1") ) 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 44d0c121d1..670ae124f2 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 @@ -12,7 +12,6 @@ import scala.concurrent.Future import dsp.errors.NotFoundException import org.knora.webapi.IRI -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.ConstructResponseUtilV2 import org.knora.webapi.messages.util.ConstructResponseUtilV2.MappingAndXSLTransformation @@ -28,8 +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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +abstract class ResponderWithStandoffV2(responderData: ResponderData) extends Responder(responderData) { /** * 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 745a7962e1..dd9d6075d2 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 @@ -50,12 +50,11 @@ import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException import org.knora.webapi.util.ApacheLuceneSupport._ -class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) - extends ResponderWithStandoffV2(responderData, appConfig) { +class SearchResponderV2(responderData: ResponderData) extends ResponderWithStandoffV2(responderData) { // A Gravsearch type inspection runner. private val gravsearchTypeInspectionRunner = - new GravsearchTypeInspectionRunner(appActor = appActor, responderData = responderData, appConfig = appConfig) + new GravsearchTypeInspectionRunner(appActor = appActor, responderData = responderData) /** * Receives a message of type [[SearchResponderRequestV2]], and returns an appropriate response message. @@ -97,7 +96,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) targetSchema, schemaOptions, requestingUser, - appConfig + responderData.appConfig ) case GravsearchCountRequestV2(query, requestingUser) => @@ -510,7 +509,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) constructClause = inputQuery.constructClause, typeInspectionResult = typeInspectionResult, querySchema = inputQuery.querySchema.getOrElse(throw AssertionException(s"WhereClause has no querySchema")), - appConfig = appConfig + appConfig = responderData.appConfig ) // TODO: if the ORDER BY criterion is a property whose occurrence is not 1, then the logic does not work correctly @@ -640,7 +639,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) valueObjectIris = allValueObjectIris, targetSchema = targetSchema, schemaOptions = schemaOptions, - appConfig = appConfig + appConfig = responderData.appConfig ) val queryPatternTransformerConstruct: ConstructToConstructTransformer = @@ -726,7 +725,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) versionDate = None, calculateMayHaveMoreResults = true, appActor = appActor, - appConfig = appConfig, + appConfig = responderData.appConfig, targetSchema = targetSchema, requestingUser = requestingUser ) @@ -835,8 +834,9 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) resourceClassIri = internalClassIri, maybeOrderByProperty = maybeInternalOrderByPropertyIri, maybeOrderByValuePredicate = maybeOrderByValuePredicate, - limit = appConfig.v2.resourcesSequence.resultsPerPage, - offset = resourcesInProjectGetRequestV2.page * appConfig.v2.resourcesSequence.resultsPerPage + limit = responderData.appConfig.v2.resourcesSequence.resultsPerPage, + offset = + resourcesInProjectGetRequestV2.page * responderData.appConfig.v2.resourcesSequence.resultsPerPage ) .toString @@ -856,7 +856,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) StandoffTagUtilV2 .getStandoffMinAndMaxStartIndexesForTextValueQuery( queryStandoff = queryStandoff, - appConfig = appConfig + appConfig = responderData.appConfig ) // Are there any matching resources? @@ -922,7 +922,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = true, appActor = appActor, targetSchema = resourcesInProjectGetRequestV2.targetSchema, - appConfig = appConfig, + appConfig = responderData.appConfig, requestingUser = resourcesInProjectGetRequestV2.requestingUser ) } yield readResourcesSequence @@ -1013,8 +1013,8 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) searchTerm = searchPhrase, limitToProject = limitToProject, limitToResourceClass = limitToResourceClass.map(_.toString), - limit = appConfig.v2.resourcesSequence.resultsPerPage, - offset = offset * appConfig.v2.resourcesSequence.resultsPerPage, + limit = responderData.appConfig.v2.resourcesSequence.resultsPerPage, + offset = offset * responderData.appConfig.v2.resourcesSequence.resultsPerPage, countQuery = false ) .toString() @@ -1069,7 +1069,7 @@ class SearchResponderV2(responderData: ResponderData, appConfig: AppConfig) calculateMayHaveMoreResults = true, appActor = appActor, targetSchema = targetSchema, - appConfig = appConfig, + appConfig = responderData.appConfig, requestingUser = requestingUser ) 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 e934b00135..940aeb1c47 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 @@ -26,7 +26,6 @@ import scala.xml.XML import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -62,8 +61,7 @@ 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, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class StandoffResponderV2(responderData: ResponderData) extends Responder(responderData) { private def xmlMimeTypes = Set( "text/xml", @@ -96,7 +94,7 @@ class StandoffResponderV2(responderData: ResponderData, appConfig: AppConfig) private val xsltCacheName = "xsltCache" private def getStandoffV2(getStandoffRequestV2: GetStandoffPageRequestV2): Future[GetStandoffResponseV2] = { - val requestMaxStartIndex = getStandoffRequestV2.offset + appConfig.standoffPerPage - 1 + val requestMaxStartIndex = getStandoffRequestV2.offset + responderData.appConfig.standoffPerPage - 1 for { resourceRequestSparql <- Future( @@ -153,7 +151,7 @@ class StandoffResponderV2(responderData: ResponderData, appConfig: AppConfig) versionDate = None, appActor = appActor, targetSchema = getStandoffRequestV2.targetSchema, - appConfig = appConfig, + appConfig = responderData.appConfig, requestingUser = getStandoffRequestV2.requestingUser ) @@ -257,7 +255,7 @@ class StandoffResponderV2(responderData: ResponderData, appConfig: AppConfig) } xsltUrl: String = - s"${appConfig.sipi.internalBaseUrl}/${resource.projectADM.shortcode}/${xsltFileValueContent.fileValue.internalFilename}/file" + s"${responderData.appConfig.sipi.internalBaseUrl}/${resource.projectADM.shortcode}/${xsltFileValueContent.fileValue.internalFilename}/file" } yield xsltUrl @@ -1279,7 +1277,7 @@ class StandoffResponderV2(responderData: ResponderData, appConfig: AppConfig) val firstTask = GetStandoffTask( resourceIri = getRemainingStandoffFromTextValueRequestV2.resourceIri, valueIri = getRemainingStandoffFromTextValueRequestV2.valueIri, - offset = appConfig.standoffPerPage, // the offset of the second page + offset = responderData.appConfig.standoffPerPage, // the offset of the second page requestingUser = getRemainingStandoffFromTextValueRequestV2.requestingUser ) 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 3d0fb542fc..46bca3e06e 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 @@ -15,7 +15,6 @@ import scala.concurrent.Future import dsp.errors._ import dsp.schema.domain.Cardinality._ import org.knora.webapi._ -import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri @@ -44,8 +43,7 @@ import org.knora.webapi.util.ActorUtil /** * Handles requests to read and write Knora values. */ -class ValuesResponderV2(responderData: ResponderData, appConfig: AppConfig) - extends Responder(responderData, appConfig) { +class ValuesResponderV2(responderData: ResponderData) extends Responder(responderData) { /** * The IRI and content of a new value or value version whose existence in the triplestore has been verified. diff --git a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala index a940f64b3d..a24652835a 100644 --- a/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/CoreSpec.scala @@ -89,7 +89,7 @@ abstract class CoreSpec // needed by some tests val appConfig = config val cacheServiceSettings = new CacheServiceSettings(appConfig) - val responderData = ResponderData(system, appActor, cacheServiceSettings) + val responderData = ResponderData(system, appActor, appConfig, cacheServiceSettings) final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala index 7521f86dc6..ed27c8c906 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToCountPrequeryTransformerSpec.scala @@ -41,8 +41,7 @@ private object CountQueryHandler { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val typeInspectionResultFuture = typeInspectionRunner.inspectTypes(constructQuery.whereClause, anythingUser) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala index bd07ed2711..78900e93f4 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/NonTriplestoreSpecificGravsearchToPrequeryTransformerSpec.scala @@ -42,8 +42,7 @@ private object QueryHandler { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val typeInspectionResultFuture = typeInspectionRunner.inspectTypes(constructQuery.whereClause, anythingUser) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala index e10d6634ec..7d077fae52 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectorSpec.scala @@ -1346,8 +1346,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = false, - appConfig = appConfig + inferTypes = false ) val parsedQuery = GravsearchParser.parseQuery(QueryWithExplicitTypeAnnotations) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1429,8 +1428,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new InferringGravsearchTypeInspector( nextInspector = None, appActor, - responderData = responderData, - appConfig = appConfig + responderData = responderData ) val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) val (_, entityInfo) = Await.result( @@ -1486,8 +1484,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new InferringGravsearchTypeInspector( nextInspector = None, appActor, - responderData = responderData, - appConfig = appConfig + responderData = responderData ) val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) val (usageIndex, entityInfo) = Await.result( @@ -1569,8 +1566,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new InferringGravsearchTypeInspector( nextInspector = None, appActor, - responderData = responderData, - appConfig = appConfig + responderData = responderData ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes3) val (usageIndex, entityInfo) = Await.result( @@ -1636,8 +1632,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes3) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1701,8 +1696,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(queryWithOptional) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1768,8 +1762,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithRedundantTypes) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1784,8 +1777,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryRdfTypeRule) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1804,8 +1796,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryKnoraObjectTypeFromPropertyIriRule) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1819,8 +1810,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfObjectFromPropertyRule) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1834,8 +1824,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryKnoraObjectTypeFromObjectRule) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1849,8 +1838,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryTypeOfSubjectFromPropertyRule) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1864,8 +1852,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryPropertyVarTypeFromFilterRule) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1879,8 +1866,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryNonPropertyVarTypeFromFilterRule) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1894,8 +1880,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryVarTypeFromFunction) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1909,8 +1894,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryIriTypeFromFunction) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1924,8 +1908,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(PathologicalQuery) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1939,8 +1922,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithRdfsLabelAndLiteral) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1954,8 +1936,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithRdfsLabelAndVariable) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1969,8 +1950,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourcesInSimpleSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1984,8 +1964,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourcesInComplexSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -1999,8 +1978,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourceIriInSimpleSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -2014,8 +1992,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryComparingResourceIriInComplexSchema) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -2029,8 +2006,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithFilterComparison) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -2044,8 +2020,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithoutAnnotation) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -2060,8 +2035,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryNonKnoraTypeWithAnnotation) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -2075,8 +2049,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithGravsearchOptions) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -2090,8 +2063,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes1) val resultFuture: Future[GravsearchTypeInspectionResult] = @@ -2106,8 +2078,7 @@ class GravsearchTypeInspectorSpec extends CoreSpec() with ImplicitSender { new GravsearchTypeInspectionRunner( appActor, responderData = responderData, - inferTypes = true, - appConfig = appConfig + inferTypes = true ) val parsedQuery = GravsearchParser.parseQuery(QueryWithInconsistentTypes2) val resultFuture: Future[GravsearchTypeInspectionResult] = diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala index a762f5aa6c..f3343cc0f2 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala @@ -145,7 +145,7 @@ class PermissionsResponderADMSpec extends CoreSpec with ImplicitSender with Priv "ask for userAdministrativePermissionsGetADM" should { "return user's administrative permissions (helper method used in queries before)" in { - val permissionsResponder = new PermissionsResponderADM(responderData, appConfig) + val permissionsResponder = new PermissionsResponderADM(responderData) val f: Future[Map[IRI, Set[PermissionADM]]] = permissionsResponder.userAdministrativePermissionsGetADM( diff --git a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala index b3150648a7..2e7e6ad190 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala @@ -5,11 +5,9 @@ package org.knora.webapi.util.cache -import akka.testkit.TestKit - -import org.knora.webapi.CoreSpec import org.knora.webapi.routing.Authenticator import org.knora.webapi.sharedtestdata.SharedTestDataV1 +import org.knora.webapi.CoreSpec class CacheUtilSpec extends CoreSpec { @@ -23,14 +21,12 @@ class CacheUtilSpec extends CoreSpec { CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.put(cacheName, sessionId, SharedTestDataV1.rootUser) CacheUtil.get(cacheName, sessionId) should be(Some(SharedTestDataV1.rootUser)) - TestKit.shutdownActorSystem(system) } "return none if key is not found " in { CacheUtil.removeAllCaches() CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.get(cacheName, 213.toString) should be(None) - TestKit.shutdownActorSystem(system) } "allow to delete a set value " in { @@ -38,7 +34,6 @@ class CacheUtilSpec extends CoreSpec { CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.remove(cacheName, sessionId) CacheUtil.get(cacheName, sessionId) should be(None) - TestKit.shutdownActorSystem(system) } } } From 5f4aa8cc7d86db586d8a705df38c2e695289bf8e Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 14:42:51 +0200 Subject: [PATCH 32/44] fix formatting --- .../src/main/scala/org/knora/webapi/core/AppServer.scala | 8 +++----- .../scala/org/knora/webapi/util/cache/CacheUtilSpec.scala | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index 9c148ceec3..06b72648b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -172,10 +172,8 @@ object AppServer { /** * Initializes the AppServer instance with the required services - * - * @param test If `true`, initiates the string formatter as test */ - def init(test: Boolean = false): ZIO[AppServerEnvironment, Nothing, AppServer] = + def init(): ZIO[AppServerEnvironment, Nothing, AppServer] = for { state <- ZIO.service[State] ts <- ZIO.service[TriplestoreService] @@ -207,7 +205,7 @@ object AppServer { val testWithSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { for { - appServer <- AppServer.init(test = true) + appServer <- AppServer.init() _ <- appServer.start(requiresRepository = false, requiresIIIFService = true) } yield () } @@ -219,7 +217,7 @@ object AppServer { val testWithoutSipi: ZLayer[AppServerEnvironment, Nothing, Unit] = ZLayer { for { - appServer <- AppServer.init(test = true) + appServer <- AppServer.init() _ <- appServer.start(requiresRepository = false, requiresIIIFService = false) } yield () } diff --git a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala index 2e7e6ad190..2b61b9612a 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala @@ -5,9 +5,9 @@ package org.knora.webapi.util.cache +import org.knora.webapi.CoreSpec import org.knora.webapi.routing.Authenticator import org.knora.webapi.sharedtestdata.SharedTestDataV1 -import org.knora.webapi.CoreSpec class CacheUtilSpec extends CoreSpec { From 07c3f6b0e6b9622c785d7dfe8dd80c013b584bb5 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 15:35:42 +0200 Subject: [PATCH 33/44] Update ResourcesRouteV2.scala --- .../scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index a19d92ab18..deadbfe32d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -698,7 +698,6 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) values.foreach { value => value.valueContent match { case fileValueContent: StillImageFileValueContentV2 => { - println("StillImageFileValueContentV2") if (!routeData.appConfig.sipi.imageMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } @@ -714,7 +713,6 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } case fileValueContent: TextFileValueContentV2 => { - println("TextFileValueContentV2") if (!routeData.appConfig.sipi.textMimeTypes.contains(fileValueContent.fileValue.internalMimeType)) { throw badRequestException(fileValueContent) } From 85df8fd5777fcd186929b5ad3c34f96b6429808d Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 16:43:50 +0200 Subject: [PATCH 34/44] refactor CacheUtilSpec --- .../org/knora/webapi/core/ActorSystem.scala | 4 +- .../knora/webapi/core/ActorSystemTest.scala | 5 +- .../webapi/util/cache/CacheUtilSpec.scala | 72 ++++++++++++++++--- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala b/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala index 2648a28822..e1a527bb3b 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/ActorSystem.scala @@ -17,7 +17,6 @@ import org.knora.webapi.store.cache.settings.CacheServiceSettings @accessible trait ActorSystem { val system: akka.actor.ActorSystem - val appConfig: AppConfig val cacheServiceSettings: CacheServiceSettings } @@ -50,8 +49,7 @@ object ActorSystem { actorSystem <- ZIO.acquireRelease(acquire(context))(release _) } yield new ActorSystem { override val system: akka.actor.ActorSystem = actorSystem - override val appConfig = config - override val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) + override val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(config) } } } diff --git a/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala b/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala index ff75388e61..662f1bff5a 100644 --- a/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala +++ b/webapi/src/test/scala/org/knora/webapi/core/ActorSystemTest.scala @@ -15,11 +15,10 @@ object ActorSystemTest { def layer(sys: akka.actor.ActorSystem): ZLayer[AppConfig, Nothing, ActorSystem] = ZLayer.scoped { for { - config <- ZIO.service[AppConfig] - context <- ZIO.executor.map(_.asExecutionContext) + appConfig <- ZIO.service[AppConfig] + context <- ZIO.executor.map(_.asExecutionContext) } yield new ActorSystem { override val system: akka.actor.ActorSystem = sys - override val appConfig = config override val cacheServiceSettings: CacheServiceSettings = new CacheServiceSettings(appConfig) } } diff --git a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala index 2b61b9612a..1b45943ac4 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala @@ -5,33 +5,87 @@ package org.knora.webapi.util.cache -import org.knora.webapi.CoreSpec +import akka.actor +import akka.testkit.TestKit +import org.scalatest.BeforeAndAfterAll +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpecLike +import zio._ +import zio.logging.backend.SLF4J + +import org.knora.webapi.config.AppConfig +import org.knora.webapi.core.LayersTest import org.knora.webapi.routing.Authenticator import org.knora.webapi.sharedtestdata.SharedTestDataV1 -class CacheUtilSpec extends CoreSpec { +class CacheUtilSpec + extends TestKit(actor.ActorSystem("CacheUtilSpec")) + with AnyWordSpecLike + with Matchers + with BeforeAndAfterAll { + + type Environment = LayersTest.DefaultTestEnvironmentWithoutSipi + + /** + * The effect layers from which the App is built. + */ + lazy val effectLayers = LayersTest.defaultLayersTestWithoutSipi + + /** + * `Bootstrap` will ensure that everything is instantiated when the Runtime is created + * and cleaned up when the Runtime is shutdown. + */ + private val bootstrap: ZLayer[ + Any, + Any, + Environment + ] = ZLayer.empty ++ Runtime.removeDefaultLoggers ++ SLF4J.slf4j ++ effectLayers + + /** + * Create a configured runtime + */ + val runtime = Unsafe.unsafe { implicit u => + Runtime.unsafe + .fromLayer(bootstrap) + } + + /** + * Create router and config by unsafe running them. + */ + private val appConfig = + Unsafe.unsafe { implicit u => + runtime.unsafe + .run( + for { config <- ZIO.service[AppConfig] } yield config + ) + .getOrThrowFiberFailure() + } + + final override def beforeAll(): Unit = { + CacheUtil.removeAllCaches() + CacheUtil.createCaches(appConfig.cacheConfigs) + } + + final override def afterAll(): Unit = { + CacheUtil.removeAllCaches() + TestKit.shutdownActorSystem(system) + } private val cacheName = Authenticator.AUTHENTICATION_INVALIDATION_CACHE_NAME - private val sessionId = System.currentTimeMillis().toString + private val sessionId = java.lang.System.currentTimeMillis().toString "Caching" should { "allow to set and get the value " in { - CacheUtil.removeAllCaches() - CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.put(cacheName, sessionId, SharedTestDataV1.rootUser) CacheUtil.get(cacheName, sessionId) should be(Some(SharedTestDataV1.rootUser)) } "return none if key is not found " in { - CacheUtil.removeAllCaches() - CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.get(cacheName, 213.toString) should be(None) } "allow to delete a set value " in { - CacheUtil.removeAllCaches() - CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.remove(cacheName, sessionId) CacheUtil.get(cacheName, sessionId) should be(None) } From 303ed62aae2ac0e331c4f96222cdd1aa0f0e5780 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 16 Sep 2022 17:24:45 +0200 Subject: [PATCH 35/44] redo refactoring on CacheUtilSpec --- .../org/knora/webapi/routing/KnoraRoute.scala | 48 +------------ .../webapi/util/cache/CacheUtilSpec.scala | 70 +++---------------- 2 files changed, 9 insertions(+), 109 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala index 53ee8e19a0..0667d7b44d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala @@ -8,7 +8,6 @@ package org.knora.webapi.routing import akka.actor.ActorRef import akka.actor.ActorSystem import akka.http.scaladsl.server.Route -import akka.pattern._ import akka.util.Timeout import com.typesafe.scalalogging.Logger import zio.prelude.Validation @@ -16,15 +15,8 @@ import zio.prelude.Validation import scala.concurrent.ExecutionContext import scala.concurrent.Future -import dsp.errors.BadRequestException -import org.knora.webapi.IRI import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter -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 -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM /** * Data that needs to be passed to each route. @@ -57,45 +49,7 @@ abstract class KnoraRoute(routeData: KnoraRouteData) { def makeRoute: Route /** - * Gets a [[ProjectADM]] corresponding to the specified project IRI. - * - * @param projectIri the project IRI. - * @param requestingUser the user making the request. - * @return the corresponding [[ProjectADM]]. - */ - protected def getProjectADM( - projectIri: IRI, - requestingUser: UserADM - ): Future[ProjectADM] = { - val checkedProjectIri = stringFormatter.validateAndEscapeProjectIri( - projectIri, - throw BadRequestException(s"Invalid project IRI: $projectIri") - ) - - if (stringFormatter.isKnoraBuiltInProjectIriStr(checkedProjectIri)) { - throw BadRequestException(s"Metadata cannot be updated for a built-in project") - } - - for { - projectInfoResponse: ProjectGetResponseADM <- - appActor - .ask( - ProjectGetRequestADM( - identifier = ProjectIdentifierADM(maybeIri = Some(checkedProjectIri)), - requestingUser = requestingUser - ) - ) - .mapTo[ProjectGetResponseADM] - } yield projectInfoResponse.project - } - - /** - * Helper method converting an [[Either]] to a [[Future]]. - */ - def toFuture[A](either: Either[Throwable, A]): Future[A] = either.fold(Future.failed, Future.successful) - - /** - * Helper method converting an [[zio.prelude.Validation]] to a [[scala.concurrent.Future]]. + * Helper method converting a [[zio.prelude.Validation]] to a [[scala.concurrent.Future]]. */ def toFuture[A](validation: Validation[Throwable, A]): Future[A] = validation.fold( diff --git a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala index 1b45943ac4..d39ae752a4 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala @@ -5,71 +5,11 @@ package org.knora.webapi.util.cache -import akka.actor -import akka.testkit.TestKit -import org.scalatest.BeforeAndAfterAll -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpecLike -import zio._ -import zio.logging.backend.SLF4J - -import org.knora.webapi.config.AppConfig -import org.knora.webapi.core.LayersTest import org.knora.webapi.routing.Authenticator import org.knora.webapi.sharedtestdata.SharedTestDataV1 +import org.knora.webapi.CoreSpec -class CacheUtilSpec - extends TestKit(actor.ActorSystem("CacheUtilSpec")) - with AnyWordSpecLike - with Matchers - with BeforeAndAfterAll { - - type Environment = LayersTest.DefaultTestEnvironmentWithoutSipi - - /** - * The effect layers from which the App is built. - */ - lazy val effectLayers = LayersTest.defaultLayersTestWithoutSipi - - /** - * `Bootstrap` will ensure that everything is instantiated when the Runtime is created - * and cleaned up when the Runtime is shutdown. - */ - private val bootstrap: ZLayer[ - Any, - Any, - Environment - ] = ZLayer.empty ++ Runtime.removeDefaultLoggers ++ SLF4J.slf4j ++ effectLayers - - /** - * Create a configured runtime - */ - val runtime = Unsafe.unsafe { implicit u => - Runtime.unsafe - .fromLayer(bootstrap) - } - - /** - * Create router and config by unsafe running them. - */ - private val appConfig = - Unsafe.unsafe { implicit u => - runtime.unsafe - .run( - for { config <- ZIO.service[AppConfig] } yield config - ) - .getOrThrowFiberFailure() - } - - final override def beforeAll(): Unit = { - CacheUtil.removeAllCaches() - CacheUtil.createCaches(appConfig.cacheConfigs) - } - - final override def afterAll(): Unit = { - CacheUtil.removeAllCaches() - TestKit.shutdownActorSystem(system) - } +class CacheUtilSpec extends CoreSpec { private val cacheName = Authenticator.AUTHENTICATION_INVALIDATION_CACHE_NAME private val sessionId = java.lang.System.currentTimeMillis().toString @@ -77,15 +17,21 @@ class CacheUtilSpec "Caching" should { "allow to set and get the value " in { + CacheUtil.removeAllCaches() + CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.put(cacheName, sessionId, SharedTestDataV1.rootUser) CacheUtil.get(cacheName, sessionId) should be(Some(SharedTestDataV1.rootUser)) } "return none if key is not found " in { + CacheUtil.removeAllCaches() + CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.get(cacheName, 213.toString) should be(None) } "allow to delete a set value " in { + CacheUtil.removeAllCaches() + CacheUtil.createCaches(appConfig.cacheConfigs) CacheUtil.remove(cacheName, sessionId) CacheUtil.get(cacheName, sessionId) should be(None) } From a810c2ebd4db34d3b3c66e45a327eb348a6b3950 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 08:52:43 +0200 Subject: [PATCH 36/44] simplify main --- webapi/src/main/scala/org/knora/webapi/Main.scala | 5 +---- .../scala/org/knora/webapi/util/cache/CacheUtilSpec.scala | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index 1d06439376..e84789188c 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -29,9 +29,6 @@ object Main extends ZIOApp { ] = ZLayer.empty ++ Runtime.removeDefaultLoggers ++ SLF4J.slf4j ++ core.LayersLive.dspLayersLive /* Here we start our Application */ - override def run = - (for { - never <- ZIO.never - } yield never).provideLayer(AppServer.live) + override def run = AppServer.live.launch } diff --git a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala index d39ae752a4..c86d9067da 100644 --- a/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/util/cache/CacheUtilSpec.scala @@ -5,9 +5,9 @@ package org.knora.webapi.util.cache +import org.knora.webapi.CoreSpec import org.knora.webapi.routing.Authenticator import org.knora.webapi.sharedtestdata.SharedTestDataV1 -import org.knora.webapi.CoreSpec class CacheUtilSpec extends CoreSpec { From d3ba0a23c0d2fd27ae13843407b14b03a817569f Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 10:38:28 +0200 Subject: [PATCH 37/44] fix stack-restart --- Makefile | 20 +++++++++---------- .../GravsearchTypeInspectionRunner.scala | 1 - .../store/iiif/impl/IIIFServiceSipiImpl.scala | 14 ++++++------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index c1e4e67607..55754e21ee 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ env-file: ## write the env file used by knora-stack. ################################# .PHONY: stack-up -stack-up: docker-build env-file ## starts the knora-stack: fuseki, sipi, redis, api. +stack-up: docker-build env-file ## starts the knora-stack: fuseki, sipi, api. @docker compose -f docker-compose.yml up -d db $(CURRENT_DIR)/webapi/scripts/wait-for-db.sh @docker compose -f docker-compose.yml up -d @@ -98,12 +98,16 @@ stack-up-fast: docker-build-knora-api-image env-file ## starts the knora-stack b .PHONY: stack-up-ci stack-up-ci: KNORA_DB_REPOSITORY_NAME := knora-test-unit -stack-up-ci: docker-build env-file print-env-file ## starts the knora-stack using 'knora-test-unit' repository: fuseki, sipi, redis, api. +stack-up-ci: docker-build env-file print-env-file ## starts the knora-stack using 'knora-test-unit' repository: fuseki, sipi, api. docker-compose -f docker-compose.yml up -d .PHONY: stack-restart -stack-restart: stack-up ## re-starts the knora-stack: fuseki, sipi, redis, api. - @docker compose -f docker-compose.yml restart +stack-restart: stack-up ## re-starts the knora-stack: fuseki, sipi, api. + @docker compose -f docker-compose.yml down + @docker compose -f docker-compose.yml up -d db + $(CURRENT_DIR)/webapi/scripts/wait-for-db.sh + @docker compose -f docker-compose.yml up -d + $(CURRENT_DIR)/webapi/scripts/wait-for-knora.sh .PHONY: stack-restart-api stack-restart-api: ## re-starts the api. Usually used after loading data into fuseki. @@ -130,10 +134,6 @@ stack-logs-sipi: ## prints out and follows the logs of the 'sipi' container runn stack-logs-sipi-no-follow: ## prints out the logs of the 'sipi' container running in knora-stack. @docker compose -f docker-compose.yml logs sipi -.PHONY: stack-logs-redis -stack-logs-redis: ## prints out and follows the logs of the 'redis' container running in knora-stack. - @docker compose -f docker-compose.yml logs -f redis - .PHONY: stack-logs-api stack-logs-api: ## prints out and follows the logs of the 'api' container running in knora-stack. @docker compose -f docker-compose.yml logs -f api @@ -164,11 +164,11 @@ stack-config: env-file ## stack without api .PHONY: stack-without-api -stack-without-api: stack-up ## starts the knora-stack without knora-api: fuseki, sipi, redis. +stack-without-api: stack-up ## starts the knora-stack without knora-api: fuseki and sipi only. @docker compose -f docker-compose.yml stop api .PHONY: stack-without-api-and-sipi -stack-without-api-and-sipi: stack-up ## starts the knora-stack without knora-api and sipi: fuseki, redis. +stack-without-api-and-sipi: stack-up ## starts the knora-stack without knora-api and sipi: fuseki only. @docker compose -f docker-compose.yml stop api @docker compose -f docker-compose.yml stop sipi 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 5254302468..193117465e 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 @@ -24,7 +24,6 @@ import org.knora.webapi.settings.KnoraDispatchers * @param appActor a reference to the application actor * @param responderData the Knora [[ResponderData]]. * @param inferTypes if true, use type inference. - * @param appConfig the application's configuration */ class GravsearchTypeInspectionRunner( appActor: ActorRef, diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala index a8b9ea05b4..cb722852f6 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala @@ -41,12 +41,12 @@ import org.knora.webapi.util.SipiUtil /** * Makes requests to Sipi. * - * @param config The application's configuration + * @param appConfig The application's configuration * @param jwt The JWT Service to handle JWT Tokens * @param httpClient The HTTP Client */ case class IIIFServiceSipiImpl( - config: AppConfig, + appConfig: AppConfig, jwt: JWTService, httpClient: CloseableHttpClient ) extends IIIFService { @@ -61,7 +61,7 @@ case class IIIFServiceSipiImpl( import SipiKnoraJsonResponseProtocol._ for { - url <- ZIO.succeed(config.sipi.internalBaseUrl + getFileMetadataRequest.filePath + "/knora.json") + url <- ZIO.succeed(appConfig.sipi.internalBaseUrl + getFileMetadataRequest.filePath + "/knora.json") request <- ZIO.succeed(new HttpGet(url)) sipiResponseStr <- doSipiRequest(request).orDie sipiResponse <- ZIO.attempt(sipiResponseStr.parseJson.convertTo[SipiKnoraJsonResponse]).orDie @@ -103,7 +103,7 @@ case class IIIFServiceSipiImpl( // builds the url for the operation def moveFileUrl(token: String) = - ZIO.succeed(s"${config.sipi.internalBaseUrl}/${config.sipi.moveFileRoute}?token=$token") + ZIO.succeed(s"${appConfig.sipi.internalBaseUrl}/${appConfig.sipi.moveFileRoute}?token=$token") // build the form to send together with the request val formParams = new util.ArrayList[NameValuePair]() @@ -149,7 +149,7 @@ case class IIIFServiceSipiImpl( def deleteUrl(token: String): ZIO[Any, Nothing, String] = ZIO.succeed( - s"${config.sipi.internalBaseUrl}/${config.sipi.deleteTempFileRoute}/${deleteTemporaryFileRequestV2.internalFilename}?token=$token" + s"${appConfig.sipi.internalBaseUrl}/${appConfig.sipi.deleteTempFileRoute}/${deleteTemporaryFileRequestV2.internalFilename}?token=$token" ) for { @@ -213,7 +213,7 @@ case class IIIFServiceSipiImpl( */ def getStatus(): UIO[IIIFServiceStatusResponse] = for { - request <- ZIO.succeed(new HttpGet(config.sipi.internalBaseUrl + "/server/test.html")) + request <- ZIO.succeed(new HttpGet(appConfig.sipi.internalBaseUrl + "/server/test.html")) response <- doSipiRequest(request).fold(_ => IIIFServiceStatusNOK, _ => IIIFServiceStatusOK) } yield response @@ -225,7 +225,7 @@ case class IIIFServiceSipiImpl( */ private def doSipiRequest(request: HttpRequest): Task[String] = { val targetHost: HttpHost = - new HttpHost(config.sipi.internalHost, config.sipi.internalPort, config.sipi.internalProtocol) + new HttpHost(appConfig.sipi.internalHost, appConfig.sipi.internalPort, appConfig.sipi.internalProtocol) val httpContext: HttpClientContext = HttpClientContext.create() var maybeResponse: Option[CloseableHttpResponse] = None From 9d064986de37d89a09f24c3876bfb7abe1cf2c53 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 11:00:44 +0200 Subject: [PATCH 38/44] fix docstring --- webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala index 62da336a58..58859618cb 100644 --- a/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/ITKnoraLiveSpec.scala @@ -37,7 +37,7 @@ import org.knora.webapi.util.LogAspect /** * This class can be used in End-to-End testing. It starts the DSP stack and - * provides access to settings and logging. + * provides access to configuration and logging. */ abstract class ITKnoraLiveSpec extends AnyWordSpec From 338445244cde663316eab4b390cb4d479eef4e3b Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 11:25:23 +0200 Subject: [PATCH 39/44] update Makefile --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 55754e21ee..0c0cd15866 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ stack-up-ci: docker-build env-file print-env-file ## starts the knora-stack usin docker-compose -f docker-compose.yml up -d .PHONY: stack-restart -stack-restart: stack-up ## re-starts the knora-stack: fuseki, sipi, api. +stack-restart: ## re-starts the knora-stack: fuseki, sipi, api. @docker compose -f docker-compose.yml down @docker compose -f docker-compose.yml up -d db $(CURRENT_DIR)/webapi/scripts/wait-for-db.sh @@ -348,11 +348,11 @@ clean-sipi-projects: ## deletes all files uploaded within a project @rm -rf sipi/images/originals/[0-9A-F][0-9A-F][0-9A-F][0-9A-F] .PHONY: check -check: # Run code formating check +check: ## Run code formating check @sbt "check" .PHONY: fmt -fmt: # Run code formating fix +fmt: ## Run code formating fix @sbt "fmt" From 892f15fd6aea594369a61c45860344c669aff702 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 13:33:08 +0200 Subject: [PATCH 40/44] Remove outdated documentation --- .../design/principles/design-overview.md | 302 +----------------- .../principles/figures/design-diagram.key | Bin 403219 -> 0 bytes .../principles/figures/design-diagram.png | Bin 74602 -> 0 bytes 3 files changed, 17 insertions(+), 285 deletions(-) delete mode 100644 docs/05-internals/design/principles/figures/design-diagram.key delete mode 100644 docs/05-internals/design/principles/figures/design-diagram.png diff --git a/docs/05-internals/design/principles/design-overview.md b/docs/05-internals/design/principles/design-overview.md index e79b970764..c33e8b530d 100644 --- a/docs/05-internals/design/principles/design-overview.md +++ b/docs/05-internals/design/principles/design-overview.md @@ -7,301 +7,36 @@ ## Introduction -Knora's responsibilities include: +DSP-API's responsibilites are: -- Receiving, validating, authenticating, and authorising HTTP requests from - clients (which may be web browsers or other software) to query or update - data in a Knora repository. -- Querying and updating the repository on behalf of clients. -- Filtering query results according to the user's permissions. -- Transforming query results into DSP-API responses. -- Ensuring that ontologies and data in the triplestore are consistent and - conform to the requirements of the - [knora-base](../../../02-knora-ontologies/knora-base.md) ontology. -- Managing the versioning of data in the triplestore. -- Working with [Sipi](http://sipi.io) to store files that cannot be stored - as RDF data. +- Querying, creating, updating, and deleting data +- Creating, updating and deleting data models (ontologies) +- Managing projects and users +- Authentication of clients +- Authorisation of clients' requests -Knora is written in [Scala](http://www.scala-lang.org/), using the +DSP-API is developed with [Scala](http://www.scala-lang.org/) and uses the [Akka](http://akka.io/) framework for message-based concurrency. It is -designed to work with any standards-compliant triplestore via -the [SPARQL 1.1 Protocol](http://www.w3.org/TR/sparql11-protocol/), but is currently -tested only with [Apache Jena Fuseki](https://jena.apache.org) (with support -for other triplestores coming soon). +designed to work with the [Apache Jena Fuseki](https://jena.apache.org) triplestore +which is compliant to the [SPARQL 1.1 Protocol](http://www.w3.org/TR/sparql11-protocol/). +For file storage, it uses [Sipi](http://sipi.io). -## Knora APIs +## DSP-API Versions -Knora supports different versions of its API for working with humanities data: +DSP-API supports different versions of its API for working with humanities data: -- [DSP-API v2](../../../03-apis/api-v2/index.md), a standards-based - API currently under development. -- [DSP-API v1](../../../03-apis/api-v1/index.md), a stable, legacy API - that focuses on maintaining compatibility with applications that used - Knora's prototype software. +- [DSP-API v2](../../../03-apis/api-v2/index.md), the actual DSP-API that should be used +- [DSP-API v1](../../../03-apis/api-v1/index.md), legacy API compatibile with applications + that used the prototype software. There is also a [Knora admin API](../../../03-apis/api-admin/index.md) for -administering Knora repositories. +administering DSP projects. -The Knora code base includes some functionality that is shared by these different +The DSP-API code base includes some functionality that is shared by these different APIs, as well as separate packages for each API. Internally, Knora APIs v1 and v2 both use functionality in the admin API. DSP-API v1 uses some functionality from API v2, but API v2 does not depend on API v1. -## Design Diagram - -![A high-level diagram of Knora.](figures/design-diagram.png "A high-level diagram of Knora.") - -## Modules - -### HTTP Module - -- `org.knora.webapi.routing`: Knora's [Akka HTTP](https://doc.akka.io/docs/akka-http/current/index.html) routes. - Each routing class matches URL patterns for requests involving some particular - type of data in the repository. Routes are API-specific. For example, - `ResourcesRouteV2` matches URL paths starting with `/v2/resources`, which - represent requests involving Knora resources. -- `org.knora.webapi.http`: a few HTTP-related constants and utilities. - -### Responders Module - -- `org.knora.webapi.responders`: Each responder is an actor that is responsible for managing - some particular type of data in the repository. A responder receives messages from - a route, does some work (e.g. querying the triplestore), and returns a reply - message. Responders are API-specific and can communicate with other responders - via messages. For example, in API v2, `ResourcesResponderV2` handles requests - involving resources, and delegates some of its tasks to `ValuesResponderV2`, - which is responsible for requests involving values. - -### Store Module - -- `org.knora.webapi.store`: Contains actors that connect to triplestores. The - most important one is `HttpTriplestoreConnector`, which communicates with - triplestores via the - [SPARQL 1.1 Protocol](http://www.w3.org/TR/sparql11-protocol/). - -### Shared Between Modules - -- `org.knora.webapi`: Contains core classes such as `Main`, which starts the - Knora server, and `SettingsImpl`, which represents the application settings - that are loaded using the [Typesafe Config](https://github.com/lightbend/config) - library. -- `org.knora.webapi.util`: Utilities needed by different parts of the application, - such as parsing and formatting tools. -- `org.knora.webapi.messages`: The Akka messages used by each responder. -- `org.knora.webapi.messages.twirl`: Text-generation templates for use with - [the Twirl template engine](https://github.com/playframework/twirl). Knora - uses Twirl to generate SPARQL requests and other types of text documents. - -## Actor Supervision and Creation - -At system start, the main application supervisor actor is created in -`LiveCore.scala`: - -```scala - /** - * The main application supervisor actor which is at the top of the actor - * hierarchy. All other actors are instantiated as child actors. Further, - * this actor is responsible for the execution of the startup and shutdown - * sequences. - */ - lazy val appActor: ActorRef = system.actorOf( - Props(new ApplicationActor with LiveManagers) - .withDispatcher(KnoraDispatchers.KnoraActorDispatcher), - name = APPLICATION_MANAGER_ACTOR_NAME - ) -``` - -and through mixin also the store and responder manager actors: - -```scala - /** - * The actor that forwards messages to actors that deal with persistent storage. - */ - lazy val storeManager: ActorRef = context.actorOf( - Props(new StoreManager(self) with LiveActorMaker) - .withDispatcher(KnoraDispatchers.KnoraActorDispatcher), - name = StoreManagerActorName - ) - - /** - * The actor that forwards messages to responder actors to handle API requests. - */ - lazy val responderManager: ActorRef = context.actorOf( - Props(new ResponderManager(self) with LiveActorMaker) - .withDispatcher(KnoraDispatchers.KnoraActorDispatcher), - name = RESPONDER_MANAGER_ACTOR_NAME - ) -``` - - -The `ApplicationActor` is the first actor in the application. All other actors -are children of this actor and thus it takes also the role of the supervisor -actor. It accepts messages for starting and stopping the Knora-API, holds the -current state of the application, and is responsible for coordination of -the startup and shutdown sequence. Further, it forwards any messages meant -for responders or the store to the respective actor. - -In most cases, there is only one instance of each supervised actor; such -actors do their work asynchronously in futures, so there would be no -advantage in using an actor pool. A few actors do have pools of instances, -because they do their work synchronously; this allows concurrency to be controlled -by setting the size of each pool. These pools are configured in `application.conf` -under `akka.actor.deployment`. - -The `ApplicationActor` also starts the HTTP service as part of the startup -sequence: - -```scala - /** - * Starts the Knora-API server. - * - * @param ignoreRepository if `true`, don't read anything from the repository on startup. - * @param requiresIIIFService if `true`, ensure that the IIIF service is started. - * @param retryCnt how many times was this command tried - */ - def appStart(ignoreRepository: Boolean, requiresIIIFService: Boolean, retryCnt: Int): Unit = { - - val bindingFuture: Future[Http.ServerBinding] = Http() - .bindAndHandle( - Route.handlerFlow(apiRoutes), - knoraSettings.internalKnoraApiHost, - knoraSettings.internalKnoraApiPort - ) - - bindingFuture onComplete { - case Success(_) => - - // Transition to ready state - self ! AppReady() - - if (knoraSettings.prometheusEndpoint) { - // Load Kamon monitoring - Kamon.loadModules() - } - - // Kick of startup procedure. - self ! InitStartUp(ignoreRepository, requiresIIIFService) - - case Failure(ex) => - if (retryCnt < 5) { - logger.error( - "Failed to bind to {}:{}! - {} - retryCnt: {}", - knoraSettings.internalKnoraApiHost, - knoraSettings.internalKnoraApiPort, - ex.getMessage, - retryCnt - ) - self ! AppStart(ignoreRepository, requiresIIIFService, retryCnt + 1) - } else { - logger.error( - "Failed to bind to {}:{}! - {}", - knoraSettings.internalKnoraApiHost, - knoraSettings.internalKnoraApiPort, - ex.getMessage - ) - self ! AppStop() - } - } - } -``` - - -## Coordinated Application Startup - -To coordinate necessary startup tasks, the application goes through a few states at startup: - - - Stopped: Application starting. Http layer is still not started. - - StartingUp: Http layer is started. Only '/health' and monitoring routes are working. - - WaitingForRepository: Repository check is initiated but not yet finished. - - RepositoryReady: Repository check has finished and repository is available. - - CreatingCaches: Creating caches is initiated but not yet finished. - - CachesReady: Caches are created and ready for use. - - LoadingOntologies: Loading of ontologies is initiated but not yet finished. - - OntologiesReady: Ontologies are loaded. - - MaintenanceMode: During backup or other maintenance tasks, so that access to the API is closed - - Running: Running state. All APIs are open. - -During the `WaitingForRepository` state, if the repository is not configured or -available, the system will indefinitely retry to access it. This allows for -prolonged startup times of the repository. Also, if checking the repository -returns an error, e.g., because the repository data needs to be migrated first, -the application will shutdown. - -## Concurrency - -In general, Knora is written in a functional style, avoiding shared mutable -state. This makes it easier to reason about concurrency, and -eliminates an important potential source of bugs (see [Out of the Tar Pit](http://curtclifton.net/papers/MoseleyMarks06a.pdf)). - -The routes and actors in Knora use Akka's `ask` pattern, -rather than the `tell` pattern, to send messages and receive responses, -because this simplifies the code considerably (using `tell` would -require actors to maintain complex mutable state), with no apparent -reduction in performance. - -To manage asynchronous communication between actors, the DSP-API -server uses Scala's `Future` monad extensively. See -[Futures with Akka](futures-with-akka.md) for details. - -We use Akka's asynchronous logging interface (see [Akka Logging](http://doc.akka.io/docs/akka/current/scala/logging.html)). - -## What the Responders Do - -In Knora, a responder is an actor that receives a -request message (a Scala case class) in the `ask` pattern, does some work -(e.g. getting data from the triplestore), and returns a reply message (another -case class). These reply messages are are defined in `org.knora.webapi.messages`. -A responder can produce a reply representing a complete API -response, or part of a response that will be used by another responder. -If it's a complete API response, there is an API-specific mechanism for -converting it into the response format that the client expects. - -## Store Module (org.knora.webapi.store package) - -The store module is used for accessing the triplestore and other -external storage providers. - -All access to the Store module goes through the `StoreManager` -supervisor actor. The `StoreManager` creates pools of actors, such as -`HttpTriplestoreActor`, that interface with the storage providers. - -The contents of the `store` package are not used directly by other -packages, which interact with the `store` package only by sending -messages to `StoreManager`. - -Parsing of SPARQL query results is handled by this module. - -See [Store Module](store-module.md) for a more detailed discussion. - -## Triplestore Access - -SPARQL queries are generated from templates, using the -[Twirl](https://github.com/playframework/twirl) template engine. For -example, if we're querying a resource, the template will contain a -placeholder for the resource's IRI. The templates can be found under -`src/main/twirl/queries/sparql`. - -To perform a SPARQL SELECT query, a responder sends a `SparqlSelectRequest` -message to the `storeManager` actor, like this: - -```scala - for { - isEntityUsedSparql <- Future(queries.sparql.v2.txt.isEntityUsed( - entityIri = entityIri, - ignoreKnoraConstraints = ignoreKnoraConstraints, - ignoreRdfSubjectAndObject = ignoreRdfSubjectAndObject - ).toString()) - - isEntityUsedResponse: SparqlSelectResponse <- (storeManager ? SparqlSelectRequest(isEntityUsedSparql)).mapTo[SparqlSelectResponse] -``` - - -The reply message, `SparqlSelectResponse`, is a data structure containing the rows -that were returned as the query result. - -To perform a SPARQL CONSTRUCT query, you can use `SparqlExtendedConstructRequest`, -and the response will be a `SparqlExtendedConstructResponse`. - ## Error Handling The error-handling design has these aims: @@ -320,9 +55,6 @@ The error-handling design has these aims: by bad input. 6. When logging errors, include the full JVM stack trace. -The design does not yet include, but could easily accommodate, -translations of error messages into different languages. - A hierarchy of exception classes is defined in `Exceptions.scala`, representing different sorts of errors that could occur. The hierarchy has two main branches: diff --git a/docs/05-internals/design/principles/figures/design-diagram.key b/docs/05-internals/design/principles/figures/design-diagram.key deleted file mode 100644 index 3ef388702593db9fe927d423c01d6ee5b93f118b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 403219 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%0D2ajaEfG75}KeJHGQ%coN*{sV< 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

d406W^sLlI0GjdOFI1R+&)U)4dTfV6h`}6O-PP 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

hxF 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 z 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/Z#08&eTC!GbmfZgP2O=UO- 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 Date: Mon, 19 Sep 2022 13:44:39 +0200 Subject: [PATCH 41/44] implement feedback from review --- .../org/knora/webapi/core/AppServer.scala | 2 +- .../store/iiif/impl/IIIFServiceSipiImpl.scala | 2 - .../config/AppConfigForTestContainers.scala | 6 +- .../e2e/v2/ResourcesRouteV2E2ESpec.scala | 147 +++++++++--------- 4 files changed, 78 insertions(+), 79 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index 06b72648b4..090f121212 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -152,7 +152,7 @@ final case class AppServer( _ <- checkCacheService _ <- ZIO.logInfo("=> Startup checks finished") _ <- ZIO.logInfo(s"DSP-API Server started: ${appConfig.knoraApi.internalKnoraApiBaseUrl}") - _ = if (appConfig.allowReloadOverHttp) ZIO.logWarning("Resetting DB over HTTP is turned ON") + _ <- if (appConfig.allowReloadOverHttp) ZIO.logWarning("Resetting DB over HTTP is turned ON") else ZIO.succeed(()) _ <- state.set(AppState.Running) } yield () } diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala index cb722852f6..0782927404 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala @@ -334,8 +334,6 @@ object IIIFServiceSipiImpl { for { config <- ZIO.service[AppConfig] jwtService <- ZIO.service[JWTService] - // HINT: Scope does not work when used together with unsafeRun to - // bridge over to Akka. TODO Need to change this as soon as Akka is removed httpClient <- ZIO.acquireRelease(acquire(config))(release(_)) } yield IIIFServiceSipiImpl(config, jwtService, httpClient) } diff --git a/webapi/src/test/scala/org/knora/webapi/config/AppConfigForTestContainers.scala b/webapi/src/test/scala/org/knora/webapi/config/AppConfigForTestContainers.scala index 48ae136173..1ab5ae2330 100644 --- a/webapi/src/test/scala/org/knora/webapi/config/AppConfigForTestContainers.scala +++ b/webapi/src/test/scala/org/knora/webapi/config/AppConfigForTestContainers.scala @@ -78,10 +78,10 @@ object AppConfigForTestContainers { _ <- ZIO.attempt(StringFormatter.initForTest()).orDie // needs early init before first usage _ <- ZIO.attempt(RdfFeatureFactory.init(appConfig)).orDie // needs early init before first usage } yield alteredConfig - }.tap(_ => ZIO.logInfo(">>> App Config for Fuseki and Sipi Testcontainers Initialized <<<")) + }.tap(_ => ZIO.logInfo(">>> AppConfig for Fuseki and Sipi Testcontainers Initialized <<<")) /** - * Altered AppConfig with ports from TestContainers for Fuseki and Sipi. + * Altered AppConfig with ports from TestContainers for Fuseki. */ val fusekiOnlyTestcontainer: ZLayer[FusekiTestContainer, Nothing, AppConfig] = ZLayer { @@ -92,5 +92,5 @@ object AppConfigForTestContainers { _ <- ZIO.attempt(StringFormatter.initForTest()).orDie // needs early init before first usage _ <- ZIO.attempt(RdfFeatureFactory.init(appConfig)).orDie // needs early init before first usage } yield alteredConfig - }.tap(_ => ZIO.logInfo(">>> App Config for Fuseki only Testcontainers Initialized <<<")) + }.tap(_ => ZIO.logInfo(">>> AppConfig for Fuseki only Testcontainers Initialized <<<")) } diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 3a8a02de3b..57a7bd44a4 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -47,6 +47,7 @@ import org.knora.webapi.routing.v2.OntologiesRouteV2 import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.util._ +import scala.collection.mutable.ArrayBuffer /** * Tests the API v2 resources route. @@ -1872,79 +1873,79 @@ class ResourcesRouteV2E2ESpec extends E2ESpec { xmlDiff.hasDifferences should be(false) } - // "read the large text without its markup, and get the markup separately as pages of standoff" ignore { // depends on previous test - // // Get the resource without markup. - // val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(hamletResourceIri.get, "UTF-8")}") - // .addHeader(new MarkupHeader(RouteUtilV2.MARKUP_STANDOFF)) ~> addCredentials( - // BasicHttpCredentials(anythingUserEmail, password) - // ) - // val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest) - // val resourceGetResponseAsString = responseToString(resourceGetResponse) - - // // Check that the response matches the ontology. - // instanceChecker.check( - // instanceResponse = resourceGetResponseAsString, - // expectedClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, - // knoraRouteGet = doGetRequest - // ) - - // // Get the standoff markup separately. - // val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) - // val textValue: JsonLDObject = - // resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasRichtext") - // val maybeTextValueAsXml: Option[String] = - // textValue.maybeString(OntologyConstants.KnoraApiV2Complex.TextValueAsXml) - // assert(maybeTextValueAsXml.isEmpty) - // val textValueIri: IRI = - // textValue.requireStringWithValidation(JsonLDKeywords.ID, stringFormatter.validateAndEscapeIri) - - // val resourceIriEncoded: IRI = URLEncoder.encode(hamletResourceIri.get, "UTF-8") - // val textValueIriEncoded: IRI = URLEncoder.encode(textValueIri, "UTF-8") - - // val standoffBuffer: ArrayBuffer[JsonLDObject] = ArrayBuffer.empty - // var offset: Int = 0 - // var hasMoreStandoff: Boolean = true - - // while (hasMoreStandoff) { - // // Get a page of standoff. - - // val standoffGetRequest = Get( - // s"$baseApiUrl/v2/standoff/$resourceIriEncoded/$textValueIriEncoded/$offset" - // ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) - // val standoffGetResponse: HttpResponse = singleAwaitingRequest(standoffGetRequest) - // val standoffGetResponseAsJsonLD: JsonLDObject = responseToJsonLDDocument(standoffGetResponse).body - - // val standoff: Seq[JsonLDValue] = - // standoffGetResponseAsJsonLD.maybeArray(JsonLDKeywords.GRAPH).map(_.value).getOrElse(Seq.empty) - - // val standoffAsJsonLDObjects: Seq[JsonLDObject] = standoff.map { - // case jsonLDObject: JsonLDObject => jsonLDObject - // case other => throw AssertionException(s"Expected JsonLDObject, got $other") - // } - - // standoffBuffer.appendAll(standoffAsJsonLDObjects) - - // standoffGetResponseAsJsonLD.maybeInt(OntologyConstants.KnoraApiV2Complex.NextStandoffStartIndex) match { - // case Some(nextOffset) => offset = nextOffset - // case None => hasMoreStandoff = false - // } - // } - - // assert(standoffBuffer.length == 6738) - - // // Check the standoff tags to make sure they match the ontology. - - // for (jsonLDObject <- standoffBuffer) { - // val docForValidation = JsonLDDocument(body = jsonLDObject).toCompactString() - - // instanceChecker.check( - // instanceResponse = docForValidation, - // expectedClassIri = - // jsonLDObject.requireStringWithValidation(JsonLDKeywords.TYPE, stringFormatter.toSmartIriWithErr), - // knoraRouteGet = doGetRequest - // ) - // } - // } + "read the large text without its markup, and get the markup separately as pages of standoff" ignore { // depends on previous test + // Get the resource without markup. + val resourceGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(hamletResourceIri.get, "UTF-8")}") + .addHeader(new MarkupHeader(RouteUtilV2.MARKUP_STANDOFF)) ~> addCredentials( + BasicHttpCredentials(anythingUserEmail, password) + ) + val resourceGetResponse: HttpResponse = singleAwaitingRequest(resourceGetRequest) + val resourceGetResponseAsString = responseToString(resourceGetResponse) + + // Check that the response matches the ontology. + instanceChecker.check( + instanceResponse = resourceGetResponseAsString, + expectedClassIri = "http://0.0.0.0:3333/ontology/0001/anything/v2#Thing".toSmartIri, + knoraRouteGet = doGetRequest + ) + + // Get the standoff markup separately. + val resourceGetResponseAsJsonLD = JsonLDUtil.parseJsonLD(resourceGetResponseAsString) + val textValue: JsonLDObject = + resourceGetResponseAsJsonLD.body.requireObject("http://0.0.0.0:3333/ontology/0001/anything/v2#hasRichtext") + val maybeTextValueAsXml: Option[String] = + textValue.maybeString(OntologyConstants.KnoraApiV2Complex.TextValueAsXml) + assert(maybeTextValueAsXml.isEmpty) + val textValueIri: IRI = + textValue.requireStringWithValidation(JsonLDKeywords.ID, stringFormatter.validateAndEscapeIri) + + val resourceIriEncoded: IRI = URLEncoder.encode(hamletResourceIri.get, "UTF-8") + val textValueIriEncoded: IRI = URLEncoder.encode(textValueIri, "UTF-8") + + val standoffBuffer: ArrayBuffer[JsonLDObject] = ArrayBuffer.empty + var offset: Int = 0 + var hasMoreStandoff: Boolean = true + + while (hasMoreStandoff) { + // Get a page of standoff. + + val standoffGetRequest = Get( + s"$baseApiUrl/v2/standoff/$resourceIriEncoded/$textValueIriEncoded/$offset" + ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val standoffGetResponse: HttpResponse = singleAwaitingRequest(standoffGetRequest) + val standoffGetResponseAsJsonLD: JsonLDObject = responseToJsonLDDocument(standoffGetResponse).body + + val standoff: Seq[JsonLDValue] = + standoffGetResponseAsJsonLD.maybeArray(JsonLDKeywords.GRAPH).map(_.value).getOrElse(Seq.empty) + + val standoffAsJsonLDObjects: Seq[JsonLDObject] = standoff.map { + case jsonLDObject: JsonLDObject => jsonLDObject + case other => throw AssertionException(s"Expected JsonLDObject, got $other") + } + + standoffBuffer.appendAll(standoffAsJsonLDObjects) + + standoffGetResponseAsJsonLD.maybeInt(OntologyConstants.KnoraApiV2Complex.NextStandoffStartIndex) match { + case Some(nextOffset) => offset = nextOffset + case None => hasMoreStandoff = false + } + } + + assert(standoffBuffer.length == 6738) + + // Check the standoff tags to make sure they match the ontology. + + for (jsonLDObject <- standoffBuffer) { + val docForValidation = JsonLDDocument(body = jsonLDObject).toCompactString() + + instanceChecker.check( + instanceResponse = docForValidation, + expectedClassIri = + jsonLDObject.requireStringWithValidation(JsonLDKeywords.TYPE, stringFormatter.toSmartIriWithErr), + knoraRouteGet = doGetRequest + ) + } + } "erase a resource" in { val resourceLastModificationDate = Instant.parse("2019-02-13T09:05:10Z") From d892d8267a3e1b039a1c8686db7332735ca013bc Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 14:25:39 +0200 Subject: [PATCH 42/44] Clearify requiresRepository --- .../org/knora/webapi/core/AppServer.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index 090f121212..ac805f966c 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -134,25 +134,25 @@ final case class AppServer( /** * Initiates the startup of the DSP-API server. * - * @param requiresRepository If `true`, checks if repository service is running, updates data if necessary and loads ontology cache. - * If `false`, checks if repository service is running but doesn't run upgrades and doesn't load ontology cache. - * @param requiresIIIFService If `true`, ensures that the IIIF service is running. + * @param requiresAdditionalRepositoryChecks If `true`, checks if repository service is running, updates data if necessary and loads ontology cache. + * If `false`, checks if repository service is running but doesn't run upgrades and doesn't load ontology cache. + * @param requiresIIIFService If `true`, ensures that the IIIF service is running. */ def start( - requiresRepository: Boolean, + requiresAdditionalRepositoryChecks: Boolean, requiresIIIFService: Boolean ) = for { _ <- ZIO.logInfo("=> Startup checks initiated") _ <- checkTriplestoreService - _ <- upgradeRepository(requiresRepository) + _ <- upgradeRepository(requiresAdditionalRepositoryChecks) _ <- buildAllCaches - _ <- populateOntologyCaches(requiresRepository) + _ <- populateOntologyCaches(requiresAdditionalRepositoryChecks) _ <- checkIIIFService(requiresIIIFService) _ <- checkCacheService _ <- ZIO.logInfo("=> Startup checks finished") _ <- ZIO.logInfo(s"DSP-API Server started: ${appConfig.knoraApi.internalKnoraApiBaseUrl}") - _ <- if (appConfig.allowReloadOverHttp) ZIO.logWarning("Resetting DB over HTTP is turned ON") else ZIO.succeed(()) + _ <- if (appConfig.allowReloadOverHttp) ZIO.logWarning("Resetting DB over HTTP is turned ON") else ZIO.unit _ <- state.set(AppState.Running) } yield () } @@ -194,7 +194,7 @@ object AppServer { ZLayer { for { appServer <- AppServer.init() - _ <- appServer.start(requiresRepository = true, requiresIIIFService = true) + _ <- appServer.start(requiresAdditionalRepositoryChecks = true, requiresIIIFService = true) } yield () } @@ -206,7 +206,7 @@ object AppServer { ZLayer { for { appServer <- AppServer.init() - _ <- appServer.start(requiresRepository = false, requiresIIIFService = true) + _ <- appServer.start(requiresAdditionalRepositoryChecks = false, requiresIIIFService = true) } yield () } @@ -218,7 +218,7 @@ object AppServer { ZLayer { for { appServer <- AppServer.init() - _ <- appServer.start(requiresRepository = false, requiresIIIFService = false) + _ <- appServer.start(requiresAdditionalRepositoryChecks = false, requiresIIIFService = false) } yield () } } From 966fce83742d6e925e956f1dc3546c849b46a520 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 14:39:36 +0200 Subject: [PATCH 43/44] use ZIO.unit instead of ZIO.succeed(()) --- .../scala/dsp/project/repo/impl/ProjectRepoLive.scala | 2 +- .../scala/dsp/project/repo/impl/ProjectRepoMock.scala | 2 +- .../src/main/scala/dsp/user/repo/impl/UserRepoLive.scala | 8 ++++---- .../src/test/scala/dsp/user/repo/impl/UserRepoMock.scala | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dsp-project/repo/src/main/scala/dsp/project/repo/impl/ProjectRepoLive.scala b/dsp-project/repo/src/main/scala/dsp/project/repo/impl/ProjectRepoLive.scala index c5b61615cc..b6c9446ea1 100644 --- a/dsp-project/repo/src/main/scala/dsp/project/repo/impl/ProjectRepoLive.scala +++ b/dsp-project/repo/src/main/scala/dsp/project/repo/impl/ProjectRepoLive.scala @@ -73,7 +73,7 @@ final case class ProjectRepoLive( (for { exists <- lookupTableShortCodeToUuid.contains(shortCode).commit _ <- if (exists) ZIO.fail(None) // project shortcode does exist - else ZIO.succeed(()) // project shortcode does not exist + else ZIO.unit // project shortcode does not exist } yield ()).tapBoth( _ => ZIO.logInfo(s"Checked for project with shortCode '${shortCode.value}', project not found."), uuid => ZIO.logInfo(s"Checked for project with shortCode '${shortCode.value}', found project with UUID '$uuid'.") diff --git a/dsp-project/repo/src/test/scala/dsp/project/repo/impl/ProjectRepoMock.scala b/dsp-project/repo/src/test/scala/dsp/project/repo/impl/ProjectRepoMock.scala index b430f0fa0e..cc35fe84c3 100644 --- a/dsp-project/repo/src/test/scala/dsp/project/repo/impl/ProjectRepoMock.scala +++ b/dsp-project/repo/src/test/scala/dsp/project/repo/impl/ProjectRepoMock.scala @@ -73,7 +73,7 @@ final case class ProjectRepoMock( (for { exists <- lookupTableShortCodeToUuid.contains(shortCode).commit _ <- if (exists) ZIO.fail(None) // project shortcode does exist - else ZIO.succeed(()) // project shortcode does not exist + else ZIO.unit // project shortcode does not exist } yield ()).tapBoth( _ => ZIO.logInfo(s"Checked for project with shortCode '$shortCode', project not found."), uuid => ZIO.logInfo(s"Checked for project with shortCode '$shortCode', found project with UUID '$uuid'.") diff --git a/dsp-user/repo/src/main/scala/dsp/user/repo/impl/UserRepoLive.scala b/dsp-user/repo/src/main/scala/dsp/user/repo/impl/UserRepoLive.scala index 1a212ad9c2..b4a8418218 100644 --- a/dsp-user/repo/src/main/scala/dsp/user/repo/impl/UserRepoLive.scala +++ b/dsp-user/repo/src/main/scala/dsp/user/repo/impl/UserRepoLive.scala @@ -91,8 +91,8 @@ final case class UserRepoLive( (for { usernameExists <- lookupTableUsernameToUuid.contains(username).commit _ <- usernameExists match { - case false => ZIO.succeed(()) // username does not exist - case true => ZIO.fail(None) // username does exist + case false => ZIO.unit // username does not exist + case true => ZIO.fail(None) // username does exist } } yield ()).tap(_ => ZIO.logInfo(s"Username '${username.value}' was checked")) @@ -103,8 +103,8 @@ final case class UserRepoLive( (for { emailExists <- lookupTableEmailToUuid.contains(email).commit _ <- emailExists match { - case false => ZIO.succeed(()) // email does not exist - case true => ZIO.fail(None) // email does exist + case false => ZIO.unit // email does not exist + case true => ZIO.fail(None) // email does exist } } yield ()).tap(_ => ZIO.logInfo(s"Email '${email.value}' was checked")) diff --git a/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala b/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala index 29a6a9eb7a..5d3fd2e776 100644 --- a/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala +++ b/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala @@ -91,7 +91,7 @@ final case class UserRepoMock( (for { usernameExists <- lookupTableUsernameToUuid.contains(username).commit _ <- usernameExists match { - case false => ZIO.succeed(()) // username does not exist + case false => ZIO.unit // username does not exist case true => ZIO.fail(None) // username does exist } } yield ()).tap(_ => ZIO.logInfo(s"Username '${username.value}' was checked")) @@ -103,7 +103,7 @@ final case class UserRepoMock( (for { emailExists <- lookupTableEmailToUuid.contains(email).commit _ <- emailExists match { - case false => ZIO.succeed(()) // email does not exist + case false => ZIO.unit // email does not exist case true => ZIO.fail(None) // email does exist } } yield ()).tap(_ => ZIO.logInfo(s"Email '${email.value}' was checked")) From b7f3e65f10167015ad67794ad9bfb52ab03569d8 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Mon, 19 Sep 2022 14:52:34 +0200 Subject: [PATCH 44/44] reformat --- .../src/test/scala/dsp/user/repo/impl/UserRepoMock.scala | 8 ++++---- .../org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala b/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala index 5d3fd2e776..93f92e285f 100644 --- a/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala +++ b/dsp-user/repo/src/test/scala/dsp/user/repo/impl/UserRepoMock.scala @@ -91,8 +91,8 @@ final case class UserRepoMock( (for { usernameExists <- lookupTableUsernameToUuid.contains(username).commit _ <- usernameExists match { - case false => ZIO.unit // username does not exist - case true => ZIO.fail(None) // username does exist + case false => ZIO.unit // username does not exist + case true => ZIO.fail(None) // username does exist } } yield ()).tap(_ => ZIO.logInfo(s"Username '${username.value}' was checked")) @@ -103,8 +103,8 @@ final case class UserRepoMock( (for { emailExists <- lookupTableEmailToUuid.contains(email).commit _ <- emailExists match { - case false => ZIO.unit // email does not exist - case true => ZIO.fail(None) // email does exist + case false => ZIO.unit // email does not exist + case true => ZIO.fail(None) // email does exist } } yield ()).tap(_ => ZIO.logInfo(s"Email '${email.value}' was checked")) diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 57a7bd44a4..da54a93b80 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -25,6 +25,7 @@ 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._ @@ -47,7 +48,6 @@ import org.knora.webapi.routing.v2.OntologiesRouteV2 import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.util._ -import scala.collection.mutable.ArrayBuffer /** * Tests the API v2 resources route.