diff --git a/docs/03-apis/feature-toggles.md b/docs/03-apis/feature-toggles.md new file mode 100644 index 0000000000..7643f12799 --- /dev/null +++ b/docs/03-apis/feature-toggles.md @@ -0,0 +1,78 @@ + + +# Feature Toggles + +Some Knora features can be turned on or off on a per-request basis. +This mechanism is based on +[Feature Toggles (aka Feature Flags)](https://martinfowler.com/articles/feature-toggles.html). + +For example, a new feature that introduces a breaking API change may first be +introduced with a feature toggle that leaves it disabled by default, so that clients +can continue using the old functionality. + +When the new feature is ready to be tested with client code, the Knora release notes +and documentation will indicate that it can be enabled on a per-request basis, as explained +below. + +At a later date, the feature may be enabled by default, and the release notes +will indicate that it can still be disabled on a per-request basis by clients +that are not yet ready to use it. + +There may be more than one version of a feature toggle. Every feature +toggle has at least one version number, which is an integer. The first +version is 1. + +Most feature toggles have an expiration date, after which they will be removed. + +## Request Header + +A client can override one or more feature toggles by submitting the HTTP header +`X-Knora-Feature-Toggles`. Its value is a comma-separated list of +toggles. Each toggle consists of: + +1. its name +2. a colon +3. the version number +4. an equals sign +5. a boolean value, which can be `on`/`off`, `yes`/`no`, or `true`/`false` + +Using `on`/`off` is recommended for clarity. For example: + +``` +X-Knora-Feature-Toggles: new-foo:2=on,new-bar=off,fast-baz:1=on +``` + +A version number must be given when enabling a toggle. +Only one version of each toggle can be enabled at a time. +If a toggle is enabled by default, and you want a version +other than the default version, simply enable the toggle, +specifying the desired version number. The version number +you specify overrides the default. + +Disabling a toggle means disabling all its versions. When +a toggle is disabled, you will get the functionality that you would have +got before the toggle existed. Therefore, a version number cannot +be given when disabling a toggle. + +## Response Header + +Knora API v2 and admin API responses contain the header +`X-Knora-Feature-Toggles`. It lists all configured toggles, +in the same format as the corresponding request header. diff --git a/docs/03-apis/index.md b/docs/03-apis/index.md index b0570f064e..4ff831a4d9 100644 --- a/docs/03-apis/index.md +++ b/docs/03-apis/index.md @@ -29,3 +29,5 @@ The Knora APIs include: administering projects that use Knora as well as Knora itself. * The Knora [Util API](api-util/index.md), which is intended to be used for information retrieval about the Knora-stack itself. + +Knora API v2 and the admin API support [Feature Toggles](feature-toggles.md). diff --git a/docs/05-internals/design/api-v2/how-to-add-a-route.md b/docs/05-internals/design/api-v2/how-to-add-a-route.md index 4107dc7cf0..425c02fa4a 100644 --- a/docs/05-internals/design/api-v2/how-to-add-a-route.md +++ b/docs/05-internals/design/api-v2/how-to-add-a-route.md @@ -64,7 +64,7 @@ See the routes in that package for examples. Typically, each route route will construct a responder request message and pass it to `RouteUtilV2.runRdfRouteWithFuture` to handle the request. -Finally, add your `knoraApiPath` function to the `apiRoutes` member +Finally, add your route's `knoraApiPath` function to the `apiRoutes` member variable in `KnoraService`. Any exception thrown inside the route will be handled by the `KnoraExceptionHandler`, so that the correct client response (including the HTTP status code) will be returned. diff --git a/docs/05-internals/design/principles/feature-toggles.md b/docs/05-internals/design/principles/feature-toggles.md new file mode 100644 index 0000000000..544036709a --- /dev/null +++ b/docs/05-internals/design/principles/feature-toggles.md @@ -0,0 +1,277 @@ + + +# Feature Toggles + +For an overview of feature toggles, see +[Feature Toggles (aka Feature Flags)](https://martinfowler.com/articles/feature-toggles.html). +The design presented here is partly inspired by that article. + +## Requirements + +- It should be possible to turn features on and off by: + + - changing a setting in `application.conf` + + - sending a particular HTTP header value with an API request + + - (in the future) using a web-based user interface to configure a + feature toggle service that multiple subsystems can access + + +- Feature implementations should be produced by factory classes, + so that the code using a feature does not need to know + about the toggling decision. + +- Feature factories should use toggle configuration taken + from different sources, without knowing where the configuration + came from. + +- An HTTP response should indicate which features are turned + on. + +- A feature toggle should have metadata such as a description, + an expiration date, developer contact information, etc. + +- A feature toggle should have a version number, so + you can get different versions of the same feature. + +- It should be possible to configure a toggle in `application.conf` + so that its setting cannot be overridden per request. + +- The design of feature toggles should avoid ambiguity and + try to prevent situations where clients might be surprised by + unexpected functionality. It should be clear what will change + when a client requests a particular toggle setting. Therefore, + per-request settings should require the client to be explicit + about what is being requested. + +## Design + +### Configuration + +### Base Configuration + +The base configuration of feature toggles is in `application.conf` +under `app.feature-toggles`. Example: + +``` +app { + feature-toggles { + new-foo { + description = "Replace the old foo routes with new ones." + + available-versions = [ 1, 2 ] + default-version = 1 + enabled-by-default = yes + override-allowed = yes + + expiration-date = "2021-12-01T00:00:00Z" + + developer-emails = [ + "A developer " + ] + } + + new-bar { + description = "Replace the old bar routes with new ones." + + available-versions = [ 1, 2, 3 ] + default-version = 3 + enabled-by-default = yes + override-allowed = yes + + expiration-date = "2021-12-01T00:00:00Z" + + developer-emails = [ + "A developer " + ] + } + + fast-baz { + description = "Replace the slower, more accurate baz route with a faster, less accurate one." + + available-versions = [ 1 ] + default-version = 1 + enabled-by-default = no + override-allowed = yes + + developer-emails = [ + "A developer " + ] + } + } +} +``` + +All fields are required except `expiration-date`. + +Since it may not be possible to predict which toggles will need versions, +all toggles must have at least one version. (If a toggle could be created +without versions, and then get versions later, it would not be obvious +what should happen if a client then requested the toggle without specifying +a version number.) Version numbers must be an ascending sequence of +consecutive integers starting from 1. + +If `expiration-date` is provided, it must be an [`xsd:dateTimeStamp`](http://www.datypic.com/sc/xsd11/t-xsd_dateTimeStamp.html). All feature toggles +should have expiration dates except for long-lived ops toggles like `fast-baz` above. + +`KnoraSettingsFeatureFactoryConfig` reads this base configuration on startup. If +a feature toggle has an expiration date in the past, a warning is logged +on startup. + +### Per-Request Configuration + +A client can override the base configuration by submitting the HTTP header +`X-Knora-Feature-Toggles`. Its value is a comma-separated list of +toggles. Each toggle consists of: + +1. its name +2. a colon +3. the version number +4. an equals sign +5. a boolean value, which can be `on`/`off`, `yes`/`no`, or `true`/`false` + +Using `on`/`off` is recommended for clarity. For example: + +``` +X-Knora-Feature-Toggles: new-foo:2=on,new-bar=off,fast-baz:1=on +``` + +A version number must be given when enabling a toggle. +Only one version of each toggle can be enabled at a time. +If a toggle is enabled by default, and you want a version +other than the default version, simply enable the toggle, +specifying the desired version number. The version number +you specify overrides the default. + +Disabling a toggle means disabling all its versions. When +a toggle is disabled, you will get the functionality that you would have +got before the toggle existed. A version number cannot +be given when disabling a toggle, because it would not +be obvious what this would mean (disable all versions +or only the specified version). + +## Response Header + +Knora API v2 and admin API responses contain the header +`X-Knora-Feature-Toggles`. It lists all configured toggles, +in the same format as the corresponding request header. + +## Implementation Framework + +A `FeatureFactoryConfig` reads feature toggles from some +configuration source, and optionally delegates to a parent +`FeatureFactoryConfig`. + +`KnoraRoute` constructs a `KnoraSettingsFeatureFactoryConfig` +to read the base configuration. For each request, it +constructs a `RequestContextFeatureFactoryConfig`, which +reads the per-request configuration and has the +`KnoraSettingsFeatureFactoryConfig` as its parent. +It then passes the per-request configuration object to the `makeRoute` +method, which can in turn pass it to a feature factory, +or send it in a request message to allow a responder to +use it. + +### Feature Factories + +The traits `FeatureFactory` and `Feature` are just tagging traits, +to make code clearer. The factory methods in a feature +factory will depend on the feature, and need only be known by +the code that uses the feature. The only requirement is that +each factory method must take a `FeatureFactoryConfig` parameter. + +To get a `FeatureToggle`, a feature factory +calls `featureFactoryConfig.getToggle`, passing the name of the toggle. +If a feature toggle has only one version, it is enough to test +whether test if the toggle is enabled, by calling `isEnabled` on the toggle. + +If the feature toggle has more than one version, call its `getMatchableState` +method. To allow the compiler to check that matches on version numbers +are exhaustive, this method is designed to be used with a sealed trait +(extending `Version`) that is implemented by case objects representing +the feature's version numbers. The method returns an instance of +`MatchableState`, which is analogous to `Option`: it is either `Off` +or `On`, and an instance of `On` contains one of the version objects. +For example: + +``` +// A trait for version numbers of the new 'foo' feature. +sealed trait NewFooVersion extends Version + +// Represents version 1 of the new 'foo' feature. +case object NEW_FOO_1 extends NewFooVersion + +// Represents version 2 of the new 'foo' feature. +case object NEW_FOO_2 extends NewFooVersion + +// The old 'foo' feature implementation. +private val oldFoo = new OldFooFeature + +// The new 'foo' feature implementation, version 1. +private val newFoo1 = new NewFooVersion1Feature + +// The new 'foo' feature implementation, version 2. +private val newFoo2 = new NewFooVersion2Feature + +def makeFoo(featureFactoryConfig: FeatureFactoryConfig): Foo = { + // Get the 'new-foo' feature toggle. + val fooToggle: FeatureToggle = featureFactoryConfig.getToggle("new-foo") + + // Choose an implementation according to the toggle state. + fooToggle.getMatchableState(NEW_FOO_1, NEW_FOO_2) match { + case Off => oldFoo + case On(NEW_FOO_1) => newFoo1 + case On(NEW_FOO_2) => newFoo2 + } +} +``` + +### Routes as Features + +To select different routes according to a feature toggle: + +- Make a feature factory that extends `KnoraRouteFactory` and `FeatureFactory`, + and has a `makeRoute` method that returns different implementations, + each of which extends `KnoraRoute` and `Feature`. + +- Make a façade route that extends `KnoraRoute`, is used in + `ApplicationActor.apiRoutes`, and has a `makeRoute` method that + delegates to the feature factory. + +To avoid constructing redundant route instances, each façade route needs its +own feature factory class. + +### Documenting a Feature Toggle + +The behaviour of each possible setting of each feature toggle should be +documented. Feature toggles that are configurable per request should be described +in the release notes. + +### Removing a Feature Toggle + +To facilitate removing a feature toggle, each implementation should have: + +- a separate file for its source code + +- a separate file for its documentation + +When the toggle is removed, the files that are no longer needed can be +deleted. diff --git a/docs/05-internals/design/principles/index.md b/docs/05-internals/design/principles/index.md index 47e2a05111..43092ca339 100644 --- a/docs/05-internals/design/principles/index.md +++ b/docs/05-internals/design/principles/index.md @@ -26,3 +26,4 @@ License along with Knora. If not, see . - [Triplestore Updates](triplestore-updates.md) - [Consistency Checking](consistency-checking.md) - [Authentication](authentication.md) +- [Feature Toggles](feature-toggles.md) diff --git a/third_party/dependencies.bzl b/third_party/dependencies.bzl index 0f617403f2..f279d8c34d 100644 --- a/third_party/dependencies.bzl +++ b/third_party/dependencies.bzl @@ -153,6 +153,7 @@ ALL_WEBAPI_MAIN_DEPENDENCIES = [ "//webapi/src/main/scala/org/knora/webapi/app", "//webapi/src/main/scala/org/knora/webapi/core", "//webapi/src/main/scala/org/knora/webapi/exceptions", + "//webapi/src/main/scala/org/knora/webapi/feature", "//webapi/src/main/scala/org/knora/webapi/http/handler", "//webapi/src/main/scala/org/knora/webapi/http/version", "//webapi/src/main/scala/org/knora/webapi/http/version/versioninfo", diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index 707eb7ff66..0ef424ec5f 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -264,6 +264,23 @@ akka-http-cors { } app { + feature-toggles { + new-list-admin-routes { + description = "Replace the old list admin routes with new ones." + + available-versions = [ 1 ] + default-version = 1 + enabled-by-default = no + override-allowed = no + expiration-date = "2021-12-01T00:00:00Z" + + developer-emails = [ + "Sepideh Alassi " + "Benjamin Geer " + ] + } + } + print-extended-config = false // If true, an extended list of configuration parameters will be printed out at startup. print-extended-config = ${?KNORA_WEBAPI_PRINT_EXTENDED_CONFIG} diff --git a/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala b/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala index 86051e14a6..e52dcd86a4 100644 --- a/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala +++ b/webapi/src/main/scala/org/knora/webapi/app/ApplicationActor.scala @@ -21,7 +21,7 @@ package org.knora.webapi.app import akka.actor.SupervisorStrategy._ import akka.actor.{Actor, ActorRef, ActorSystem, OneForOneStrategy, Props, Stash, Timers} -import akka.http.scaladsl.Http +import akka.http.scaladsl.{Http, server} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{ExceptionHandler, RejectionHandler, Route} import akka.stream.Materializer @@ -96,8 +96,6 @@ trait LiveManagers extends Managers { class ApplicationActor extends Actor with Stash with LazyLogging with AroundDirectives with Timers { this: Managers => - private val log = akka.event.Logging(context.system, this.getClass) - logger.debug("entered the ApplicationManager constructor") implicit val system: ActorSystem = context.system @@ -122,11 +120,6 @@ class ApplicationActor extends Actor with Stash with LazyLogging with AroundDire */ implicit protected val timeout: Timeout = knoraSettings.defaultTimeout - /** - * A user representing the Knora API server, used for initialisation on startup. - */ - private val systemUser = KnoraSystemInstances.Users.SystemUser - /** * Route data. */ @@ -404,7 +397,7 @@ class ApplicationActor extends Actor with Stash with LazyLogging with AroundDire val exceptionHandler: ExceptionHandler = handler.KnoraExceptionHandler(KnoraSettings(system)) // Combining the two handlers for convenience - val handleErrors = handleRejections(rejectionHandler) & handleExceptions(exceptionHandler) + val handleErrors: server.Directive[Unit] = handleRejections(rejectionHandler) & handleExceptions(exceptionHandler) /** * All routes composed together and CORS activated based on the diff --git a/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala b/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala index a0eafc268b..b506339a24 100644 --- a/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala +++ b/webapi/src/main/scala/org/knora/webapi/exceptions/Exceptions.scala @@ -432,7 +432,7 @@ object SipiException { * * @param message a description of the error. */ -abstract class ApplicationConfigurationException(message: String) extends Exception(message) with KnoraException +abstract class ApplicationConfigurationException(message: String, cause: Option[Throwable] = None) extends Exception(message, cause.orNull) with KnoraException object ApplicationConfigurationException { // So we can match instances of ApplicationConfigurationException, even though it's an abstract class @@ -444,7 +444,7 @@ object ApplicationConfigurationException { * * @param message a description of the error. */ -case class UnsuportedTriplestoreException(message: String) extends ApplicationConfigurationException(message) +case class UnsupportedTriplestoreException(message: String) extends ApplicationConfigurationException(message) /** * Indicates that the HTTP configuration is incorrect. @@ -460,6 +460,12 @@ case class HttpConfigurationException(message: String) extends ApplicationConfig */ case class TestConfigurationException(message: String) extends ApplicationConfigurationException(message) +/** + * Indicates that a feature toggle configuration is incorrect. + * + * @param message a description of the error. + */ +case class FeatureToggleException(message: String, cause: Option[Throwable] = None) extends ApplicationConfigurationException(message) /** * Helper functions for error handling. diff --git a/webapi/src/main/scala/org/knora/webapi/feature/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/feature/BUILD.bazel new file mode 100644 index 0000000000..28ecf4f484 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/feature/BUILD.bazel @@ -0,0 +1,20 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library") + +scala_library( + name = "feature", + srcs = glob(["**/*.scala"]), + unused_dependency_checker_mode = "warn", + deps = [ + "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/exceptions", + "//webapi/src/main/scala/org/knora/webapi/messages", + "//webapi/src/main/scala/org/knora/webapi/settings", + "@maven//:com_typesafe_akka_akka_actor_2_12", + "@maven//:com_typesafe_akka_akka_http_2_12", + "@maven//:com_typesafe_akka_akka_http_core_2_12", + "@maven//:com_typesafe_scala_logging_scala_logging_2_12", + "@maven//:org_apache_jena_apache_jena_libs", + ], +) diff --git a/webapi/src/main/scala/org/knora/webapi/feature/FeatureFactory.scala b/webapi/src/main/scala/org/knora/webapi/feature/FeatureFactory.scala new file mode 100644 index 0000000000..c956c84b3c --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/feature/FeatureFactory.scala @@ -0,0 +1,439 @@ +/* + * Copyright © 2015-2018 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi.feature + +import akka.http.scaladsl.model.{HttpHeader, HttpResponse} +import akka.http.scaladsl.model.headers.RawHeader +import akka.http.scaladsl.server.RequestContext +import org.knora.webapi.exceptions.{BadRequestException, FeatureToggleException} +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.settings.KnoraSettings.FeatureToggleBaseConfig +import org.knora.webapi.settings.KnoraSettingsImpl + +import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} + +/** + * A tagging trait for module-specific factories that produce implementations of features. + */ +trait FeatureFactory + +/** + * A tagging trait for classes that implement features returned by feature factories. + */ +trait Feature + +/** + * A tagging trait for case objects representing feature versions. + */ +trait Version + +/** + * A trait representing the state of a feature toggle. + */ +sealed trait FeatureToggleState + +/** + * Indicates that a feature toggle is off. + */ +case object ToggleStateOff extends FeatureToggleState + +/** + * Indicates that a feature toggle is on. + * + * @param version the configured version of the toggle. + */ +case class ToggleStateOn(version: Int) extends FeatureToggleState + +/** + * Represents a feature toggle state, for use in match-case expressions. + */ +sealed trait MatchableState[+T] + +/** + * A matchable object indicating that a feature toggle is off. + */ +case object Off extends MatchableState[Nothing] + +/** + * A matchable object indicating that a feature toggle is on. + * + * @param versionObj a case object representing the enabled version of the toggle. + * @tparam T the type of the case object. + */ +case class On[T <: Version](versionObj: T) extends MatchableState[T] + +/** + * Represents a feature toggle. + * + * @param featureName the name of the feature toggle. + * @param state the state of the feature toggle. + */ +case class FeatureToggle(featureName: String, + state: FeatureToggleState) { + /** + * Returns `true` if this toggle is enabled. + */ + def isEnabled: Boolean = { + state match { + case ToggleStateOn(_) => true + case ToggleStateOff => false + } + } + + /** + * Returns a [[MatchableState]] indicating the state of this toggle, for use in match-case expressions. + * + * @param versionObjects case objects representing the supported versions of the feature, in ascending + * order by version number. + * @tparam T a sealed trait implemented by the version objects. + * @return one of the objects in `versionObjects`, or [[Off]]. + */ + def getMatchableState[T <: Version](versionObjects: T*): MatchableState[T] = { + state match { + case ToggleStateOn(version) => + if (version < 1) { + // Shouldn't happen; this error should have been caught already. + throw FeatureToggleException(s"Invalid version number $version for toggle $featureName") + } + + if (versionObjects.size < version) { + // The caller didn't pass enough version objects. + throw FeatureToggleException(s"Not enough version objects for $featureName") + } + + // Return the version object whose position in the sequence corresponds to the configured version. + // This relies on the fact that version numbers must be an ascending sequence of consecutive + // integers starting from 1. + On(versionObjects(version - 1)) + + case ToggleStateOff => Off + } + } +} + +object FeatureToggle { + /** + * The name of the HTTP request header containing feature toggles. + */ + val REQUEST_HEADER: String = "X-Knora-Feature-Toggles" + val REQUEST_HEADER_LOWERCASE: String = REQUEST_HEADER.toLowerCase + + /** + * The name of the HTTP response header that lists configured feature toggles. + */ + val RESPONSE_HEADER: String = REQUEST_HEADER + val RESPONSE_HEADER_LOWERCASE: String = REQUEST_HEADER_LOWERCASE + + /** + * Constructs a default [[FeatureToggle]] from a [[FeatureToggleBaseConfig]]. + * + * @param baseConfig a feature toggle's base configuration. + * @return a [[FeatureToggle]] representing the feature's default setting. + */ + def fromBaseConfig(baseConfig: FeatureToggleBaseConfig): FeatureToggle = { + FeatureToggle( + featureName = baseConfig.featureName, + state = if (baseConfig.enabledByDefault) { + ToggleStateOn(baseConfig.defaultVersion) + } else { + ToggleStateOff + } + ) + } + + /** + * Constructs a feature toggle from non-base configuration. + * + * @param featureName the name of the feature. + * @param isEnabled `true` if the feature should be enabled. + * @param maybeVersion the version of the feature that should be used. + * @param baseConfig the base configuration of the toggle. + * @return a [[FeatureToggle]] for the toggle. + */ + def apply(featureName: String, + isEnabled: Boolean, + maybeVersion: Option[Int], + baseConfig: FeatureToggleBaseConfig): FeatureToggle = { + if (!baseConfig.overrideAllowed) { + throw BadRequestException(s"Feature toggle $featureName cannot be overridden") + } + + for (version: Int <- maybeVersion) { + if (!baseConfig.availableVersions.contains(version)) { + throw BadRequestException(s"Feature toggle $featureName has no version $version") + } + } + + val state: FeatureToggleState = (isEnabled, maybeVersion) match { + case (true, Some(definedVersion)) => ToggleStateOn(definedVersion) + case (false, None) => ToggleStateOff + case (true, None) => throw BadRequestException(s"You must specify a version number to enable feature toggle $featureName") + case (false, Some(_)) => throw BadRequestException(s"You cannot specify a version number when disabling feature toggle $featureName") + } + + FeatureToggle( + featureName = featureName, + state = state + ) + } +} + +/** + * An abstract class representing configuration for a [[FeatureFactory]] from a particular + * configuration source. + * + * @param maybeParent if this [[FeatureFactoryConfig]] has no setting for a particular + * feature toggle, it delegates to its parent. + */ +abstract class FeatureFactoryConfig(protected val maybeParent: Option[FeatureFactoryConfig]) { + /** + * Gets the base configuration for a feature toggle. + * + * @param featureName the name of the feature. + * @return the toggle's base configuration. + */ + protected[feature] def getBaseConfig(featureName: String): FeatureToggleBaseConfig + + /** + * Gets the base configurations of all feature toggles. + */ + protected[feature] def getAllBaseConfigs: Set[FeatureToggleBaseConfig] + + /** + * Returns a feature toggle in the configuration source of this [[FeatureFactoryConfig]]. + * + * @param featureName the name of a feature. + * @return the configuration of the feature toggle in this [[FeatureFactoryConfig]]'s configuration + * source, or `None` if the source contains no configuration for that feature toggle. + */ + protected[feature] def getLocalConfig(featureName: String): Option[FeatureToggle] + + /** + * Returns an [[HttpHeader]] giving the state of all feature toggles. + */ + def makeHttpResponseHeader: Option[HttpHeader] = { + // Convert each toggle to its string representation. + val enabledToggles: Set[String] = getAllBaseConfigs.map { + baseConfig: FeatureToggleBaseConfig => + val featureToggle: FeatureToggle = getToggle(baseConfig.featureName) + + val toggleStateStr: String = featureToggle.state match { + case ToggleStateOn(version) => s":$version=on" + case ToggleStateOff => s"=off" + } + + s"${featureToggle.featureName}$toggleStateStr" + } + + // Are any toggles enabled? + if (enabledToggles.nonEmpty) { + // Yes. Return a header. + Some(RawHeader(FeatureToggle.RESPONSE_HEADER, enabledToggles.mkString(","))) + } else { + // No. Don't return a header. + None + } + } + + /** + * Adds an [[HttpHeader]] to an [[HttpResponse]] indicating which feature toggles are enabled. + */ + def addHeaderToHttpResponse(httpResponse: HttpResponse): HttpResponse = { + makeHttpResponseHeader match { + case Some(header) => httpResponse.withHeaders(header) + case None => httpResponse + } + } + + /** + * Returns a feature toggle, taking into account the base configuration + * and the parent configuration. + * + * @param featureName the name of the feature. + * @return the feature toggle. + */ + @tailrec + final def getToggle(featureName: String): FeatureToggle = { + // Get the base configuration for the feature. + val baseConfig: FeatureToggleBaseConfig = getBaseConfig(featureName) + + // Do we represent the base configuration? + maybeParent match { + case None => + // Yes. Return our setting. + FeatureToggle.fromBaseConfig(baseConfig) + + case Some(parent) => + // No. Can the default setting be overridden? + if (baseConfig.overrideAllowed) { + // Yes. Do we have a setting for this feature? + getLocalConfig(featureName) match { + case Some(setting) => + // Yes. Return our setting. + setting + + case None => + // We don't have a setting for this feature. Delegate to the parent. + parent.getToggle(featureName) + } + } else { + // The default setting can't be overridden. Return it. + FeatureToggle.fromBaseConfig(baseConfig) + } + } + } +} + +/** + * A [[FeatureFactoryConfig]] that reads configuration from the application's configuration file. + * + * @param knoraSettings a [[KnoraSettingsImpl]] representing the configuration in the application's + * configuration file. + */ +class KnoraSettingsFeatureFactoryConfig(knoraSettings: KnoraSettingsImpl) extends FeatureFactoryConfig(None) { + private val baseConfigs: Map[String, FeatureToggleBaseConfig] = knoraSettings.featureToggles.map { + baseConfig => baseConfig.featureName -> baseConfig + }.toMap + + override protected[feature] def getBaseConfig(featureName: String): FeatureToggleBaseConfig = { + baseConfigs.getOrElse(featureName, throw BadRequestException(s"No such feature: $featureName")) + } + + override protected[feature] def getAllBaseConfigs: Set[FeatureToggleBaseConfig] = { + baseConfigs.values.toSet + } + + override protected[feature] def getLocalConfig(featureName: String): Option[FeatureToggle] = { + Some(FeatureToggle.fromBaseConfig(getBaseConfig(featureName))) + } +} + +/** + * An abstract class for feature factory configs that don't represent the base configuration. + * + * @param parent the parent config. + */ +abstract class OverridingFeatureFactoryConfig(parent: FeatureFactoryConfig) extends FeatureFactoryConfig(Some(parent)) { + protected val featureToggles: Map[String, FeatureToggle] + + override protected[feature] def getBaseConfig(featureName: String): FeatureToggleBaseConfig = { + parent.getBaseConfig(featureName) + } + + override protected[feature] def getAllBaseConfigs: Set[FeatureToggleBaseConfig] = { + parent.getAllBaseConfigs + } + + override protected[feature] def getLocalConfig(featureName: String): Option[FeatureToggle] = { + featureToggles.get(featureName) + } +} + +object RequestContextFeatureFactoryConfig { + // Strings that we accept as Boolean true values. + val TRUE_STRINGS: Set[String] = Set("true", "yes", "on") + + // Strings that we accept as Boolean false values. + val FALSE_STRINGS: Set[String] = Set("false", "no", "off") +} + +/** + * A [[FeatureFactoryConfig]] that reads configuration from a header in an HTTP request. + * + * @param requestContext the HTTP request context. + * @param parent the parent [[FeatureFactoryConfig]]. + */ +class RequestContextFeatureFactoryConfig(requestContext: RequestContext, + parent: FeatureFactoryConfig)(implicit stringFormatter: StringFormatter) extends OverridingFeatureFactoryConfig(parent) { + + import FeatureToggle._ + import RequestContextFeatureFactoryConfig._ + + private def invalidHeaderValue: Nothing = throw BadRequestException(s"Invalid value for header $REQUEST_HEADER") + + // Read feature toggles from an HTTP header. + protected override val featureToggles: Map[String, FeatureToggle] = Try { + // Was the feature toggle header submitted? + requestContext.request.headers.find(_.lowercaseName == REQUEST_HEADER_LOWERCASE) match { + case Some(featureToggleHeader: HttpHeader) => + // Yes. Parse it into comma-separated key-value pairs, each representing a feature toggle. + val toggleSeq: Seq[(String, FeatureToggle)] = featureToggleHeader.value.split(',').map { + headerValueItem: String => + headerValueItem.split('=').map(_.trim) match { + case Array(featureNameAndVersionStr: String, isEnabledStr: String) => + val featureNameAndVersion: Array[String] = featureNameAndVersionStr.split(':').map(_.trim) + val featureName: String = featureNameAndVersion.head + + // Accept the boolean values that are accepted in application.conf. + val isEnabled: Boolean = if (TRUE_STRINGS.contains(isEnabledStr.toLowerCase)) { + true + } else if (FALSE_STRINGS.contains(isEnabledStr.toLowerCase)) { + false + } else { + throw BadRequestException(s"Invalid boolean '$isEnabledStr' in feature toggle $featureName") + } + + val maybeVersion: Option[Int] = featureNameAndVersion.drop(1).headOption.map { + versionStr => stringFormatter.validateInt(versionStr, throw BadRequestException(s"Invalid version number '$versionStr' in feature toggle $featureName")) + } + + featureName -> FeatureToggle( + featureName = featureName, + isEnabled = isEnabled, + maybeVersion = maybeVersion, + baseConfig = parent.getBaseConfig(featureName) + ) + + case _ => invalidHeaderValue + } + }.toSeq + + if (toggleSeq.size > toggleSeq.map(_._1).toSet.size) { + throw BadRequestException(s"You cannot set the same feature toggle more than once per request") + } + + toggleSeq.toMap + + case None => + // No feature toggle header was submitted. + Map.empty[String, FeatureToggle] + } + } match { + case Success(parsedToggles) => parsedToggles + + case Failure(ex) => + ex match { + case badRequest: BadRequestException => throw badRequest + case _ => invalidHeaderValue + } + } +} + +/** + * A [[FeatureFactoryConfig]] with a fixed configuration, to be used in tests. + * + * @param testToggles the toggles to be used. + */ +class TestFeatureFactoryConfig(testToggles: Set[FeatureToggle], parent: FeatureFactoryConfig) extends OverridingFeatureFactoryConfig(parent) { + protected override val featureToggles: Map[String, FeatureToggle] = testToggles.map { + setting => setting.featureName -> setting + }.toMap +} diff --git a/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala b/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala index b26f655b46..2eebde91c5 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala @@ -34,7 +34,7 @@ object ServerVersion { private val AkkaNameAndVersion = s"akka-http/${VersionInfo.akkaHttpVersion}" private val AllProducts = ApiNameAndVersion + " " + AkkaNameAndVersion - def serverVersionHeader(): Server = Server(products = AllProducts) + def serverVersionHeader: Server = Server(products = AllProducts) def addServerHeader(route: Route): Route = respondWithHeader(serverVersionHeader) { route diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala index 1dcaeb50ab..8a8fd5c164 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala @@ -22,13 +22,13 @@ package org.knora.webapi.messages.admin.responder.sipimessages import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectRestrictedViewSettingsADM, ProjectsADMJsonProtocol} import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.v1.responder.{KnoraRequestV1, KnoraResponseV1} +import org.knora.webapi.messages.admin.responder.{KnoraRequestADM, KnoraResponseADM} import spray.json.{DefaultJsonProtocol, JsValue, NullOptions, RootJsonFormat} /** * An abstract trait representing a Knora v1 API request message that can be sent to `SipiResponderV2`. */ -sealed trait SipiResponderRequestADM extends KnoraRequestV1 +sealed trait SipiResponderRequestADM extends KnoraRequestADM /** * A Knora v1 API request message that requests information about a `FileValue`. @@ -46,8 +46,7 @@ case class SipiFileInfoGetRequestADM(projectID: String, filename: String, reques * @param restrictedViewSettings the project's restricted view settings. */ case class SipiFileInfoGetResponseADM(permissionCode: Int, - restrictedViewSettings: Option[ProjectRestrictedViewSettingsADM], - ) extends KnoraResponseV1 { + restrictedViewSettings: Option[ProjectRestrictedViewSettingsADM]) extends KnoraResponseADM { def toJsValue: JsValue = SipiResponderResponseADMJsonProtocol.sipiFileInfoGetResponseADMFormat.write(this) } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/BUILD.bazel b/webapi/src/main/scala/org/knora/webapi/routing/BUILD.bazel index 2765f9e733..9c16bfcd33 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/BUILD.bazel +++ b/webapi/src/main/scala/org/knora/webapi/routing/BUILD.bazel @@ -8,6 +8,7 @@ scala_library( unused_dependency_checker_mode = "warn", deps = [ "//webapi/src/main/scala/org/knora/webapi", + "//webapi/src/main/scala/org/knora/webapi/feature", "//webapi/src/main/scala/org/knora/webapi/annotation", "//webapi/src/main/scala/org/knora/webapi/exceptions", "//webapi/src/main/scala/org/knora/webapi/http/status", 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 07f9d5a178..387ded1fbd 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/HealthRoute.scala @@ -24,6 +24,7 @@ import akka.http.scaladsl.server.Directives.{get, path} import akka.http.scaladsl.server.Route import akka.pattern.ask import akka.util.Timeout +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.app.appmessages.{AppState, AppStates, GetAppState} import spray.json.{JsObject, JsString} @@ -120,7 +121,7 @@ class HealthRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("health") { get { requestContext => 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 0d73365ce8..82b49429af 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala @@ -21,12 +21,13 @@ package org.knora.webapi.routing import akka.actor.{ActorRef, ActorSystem} import akka.event.LoggingAdapter -import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.{RequestContext, Route, RouteResult} import akka.pattern._ import akka.stream.Materializer import akka.util.Timeout import org.knora.webapi.IRI import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.{FeatureFactoryConfig, KnoraSettingsFeatureFactoryConfig, RequestContextFeatureFactoryConfig} import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectADM, ProjectGetRequestADM, ProjectGetResponseADM, ProjectIdentifierADM} import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -46,10 +47,12 @@ case class KnoraRouteData(system: ActorSystem, /** - * An abstract class providing values that are commonly used in Knora responders. + * An abstract class providing functionality that is commonly used by Knora routes and by + * feature factories that construct Knora routes. + * + * @param routeData a [[KnoraRouteData]] providing access to the application. */ -abstract class KnoraRoute(routeData: KnoraRouteData) { - +abstract class KnoraRouteFactory(routeData: KnoraRouteData) { implicit protected val system: ActorSystem = routeData.system implicit protected val settings: KnoraSettingsImpl = KnoraSettings(system) implicit protected val timeout: Timeout = settings.defaultTimeout @@ -64,11 +67,60 @@ abstract class KnoraRoute(routeData: KnoraRouteData) { protected val baseApiUrl: String = settings.internalKnoraApiBaseUrl /** - * Returns the route. Needs to be implemented in each subclass. + * Constructs a route. This can be done: + * + * - by statically returning a routing function (if this is an ordinary route that + * doesn't use a feature factory, or if this is a route feature returned by + * a feature factory) * - * @return [[Route]] + * - by asking a feature factory for a routing function (if this is a façade route) + * + * - by making a choice based on a feature toggle (if this is a feature factory) + * + * @param featureFactoryConfig the per-request feature factory configuration. + * @return a route configured with the features enabled by the feature factory configuration. */ - def knoraApiPath: Route + def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route +} + +/** + * 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) extends KnoraRouteFactory(routeData) { + + /** + * A [[KnoraSettingsFeatureFactoryConfig]] to use as the parent [[FeatureFactoryConfig]]. + */ + private val knoraSettingsFeatureFactoryConfig: KnoraSettingsFeatureFactoryConfig = new + KnoraSettingsFeatureFactoryConfig(settings) + + /** + * Returns a routing function that uses per-request feature factory configuration. + */ + def knoraApiPath: Route = runRoute + + /** + * A routing function that calls `makeRoute`, passing it the per-request feature factory configuration, + * and runs the resulting routing function. + * + * @param requestContext the HTTP request context. + * @return the result of running the route. + */ + private def runRoute(requestContext: RequestContext): Future[RouteResult] = { + // Construct the per-request feature factory configuration. + val featureFactoryConfig: FeatureFactoryConfig = new RequestContextFeatureFactoryConfig( + requestContext = requestContext, + parent = knoraSettingsFeatureFactoryConfig + ) + + // Construct a routing function using that configuration. + val route: Route = makeRoute(featureFactoryConfig) + + // Call the routing function. + route(requestContext) + } /** * Gets a [[ProjectADM]] corresponding to the specified project IRI. 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 8b3386890d..68f5b27077 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RejectingRoute.scala @@ -20,6 +20,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.pattern.ask import akka.util.Timeout +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.app.appmessages.{AppState, AppStates, GetAppState} import scala.concurrent.Future @@ -35,7 +36,7 @@ trait AppStateAccess { override implicit val timeout: Timeout = 2998.millis - protected def getAppState(): Future[AppState] = for { + protected def getAppState: Future[AppState] = for { state <- (applicationActor ? GetAppState()).mapTo[AppState] @@ -55,7 +56,7 @@ class RejectingRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path(Remaining) { wholePath => @@ -68,10 +69,9 @@ class RejectingRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi } } - onComplete(getAppState()) { - - case Success(appState) => { + onComplete(getAppState) { + case Success(appState) => appState match { case AppStates.Running if rejectSeq.flatten.nonEmpty => // route not allowed. will complete request. @@ -89,12 +89,10 @@ class RejectingRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi log.info(msg) complete(StatusCodes.ServiceUnavailable, msg) } - } - case Failure(ex) => { + case Failure(ex) => log.error("RejectingRoute - ex: {}", ex) complete(StatusCodes.ServiceUnavailable, ex.getMessage) - } } } } 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 efbd0e479c..b566a3fd90 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilADM.scala @@ -22,11 +22,13 @@ package org.knora.webapi.routing import akka.actor.ActorRef import akka.event.LoggingAdapter import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.server.{RequestContext, RouteResult} import akka.pattern._ import akka.util.Timeout import org.knora.webapi._ import org.knora.webapi.exceptions.UnexpectedMessageException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.{KnoraRequestADM, KnoraResponseADM} import org.knora.webapi.settings.KnoraSettingsImpl @@ -41,21 +43,24 @@ object RouteUtilADM { /** * Sends a message to a responder and completes the HTTP request by returning the response as JSON. * - * @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 responderManager a reference to the responder manager. - * @param log a logging adapter. - * @param timeout a timeout for `ask` messages. - * @param executionContext an execution context for futures. + * @param requestMessageF a future containing a [[KnoraRequestADM]] message that should be sent to the responder manager. + * @param requestContext the akka-http [[RequestContext]]. + * @param featureFactoryConfig the per-request feature factory configuration. + * @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. + * @param executionContext an execution context for futures. * @return a [[Future]] containing a [[RouteResult]]. */ def runJsonRoute(requestMessageF: Future[KnoraRequestADM], requestContext: RequestContext, + featureFactoryConfig: FeatureFactoryConfig, settings: KnoraSettingsImpl, responderManager: ActorRef, log: LoggingAdapter) - (implicit timeout: Timeout, executionContext: ExecutionContext): Future[RouteResult] = { + (implicit timeout: Timeout, + executionContext: ExecutionContext): Future[RouteResult] = { val httpResponse: Future[HttpResponse] = for { @@ -82,16 +87,16 @@ object RouteUtilADM { } jsonResponse = knoraResponse.toJsValue.asJsObject - - } yield HttpResponse( - status = StatusCodes.OK, - entity = HttpEntity( - ContentTypes.`application/json`, - jsonResponse.compactPrint + } yield featureFactoryConfig.addHeaderToHttpResponse( + HttpResponse( + status = StatusCodes.OK, + entity = HttpEntity( + ContentTypes.`application/json`, + jsonResponse.compactPrint + ) ) ) requestContext.complete(httpResponse) } - } 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 d141e22e8f..5527852d8d 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala @@ -28,6 +28,7 @@ import akka.util.Timeout import org.apache.jena import org.knora.webapi._ import org.knora.webapi.exceptions.{BadRequestException, UnexpectedMessageException} +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.util.{JsonLDDocument, RdfFormatUtil} import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceTEIGetResponseV2 @@ -178,18 +179,20 @@ object RouteUtilV2 { /** * Sends a message to a responder and completes the HTTP request by returning the response as RDF using content negotiation. * - * @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 log a logging adapter. - * @param targetSchema the API schema that should be used in the response. - * @param timeout a timeout for `ask` messages. - * @param executionContext an execution context for futures. + * @param requestMessage a future containing a [[KnoraRequestV2]] message that should be sent to the responder manager. + * @param requestContext the akka-http [[RequestContext]]. + * @param featureFactoryConfig the per-request feature factory configuration. + * @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. + * @param timeout a timeout for `ask` messages. + * @param executionContext an execution context for futures. * @return a [[Future]] containing a [[RouteResult]]. */ private def runRdfRoute(requestMessage: KnoraRequestV2, requestContext: RequestContext, + featureFactoryConfig: FeatureFactoryConfig, settings: KnoraSettingsImpl, responderManager: ActorRef, log: LoggingAdapter, @@ -233,12 +236,14 @@ object RouteUtilV2 { settings = settings, schemaOptions = schemaOptions ) - } yield HttpResponse( - status = StatusCodes.OK, - - entity = HttpEntity( - contentType, - formattedResponseContent + } yield featureFactoryConfig.addHeaderToHttpResponse( + HttpResponse( + status = StatusCodes.OK, + + entity = HttpEntity( + contentType, + formattedResponseContent + ) ) ) @@ -248,18 +253,20 @@ object RouteUtilV2 { /** * Sends a message to a responder and completes the HTTP request by returning the response as TEI/XML. * - * @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. - * @param timeout a timeout for `ask` messages. - * @param executionContext an execution context for futures. + * @param requestMessageF a future containing a [[KnoraRequestV2]] message that should be sent to the responder manager. + * @param requestContext the akka-http [[RequestContext]]. + * @param featureFactoryConfig the per-request feature factory configuration. + * @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. + * @param timeout a timeout for `ask` messages. + * @param executionContext an execution context for futures. * @return a [[Future]] containing a [[RouteResult]]. */ def runTEIXMLRoute(requestMessageF: Future[KnoraRequestV2], requestContext: RequestContext, + featureFactoryConfig: FeatureFactoryConfig, settings: KnoraSettingsImpl, responderManager: ActorRef, log: LoggingAdapter, @@ -282,11 +289,13 @@ object RouteUtilV2 { } - } yield HttpResponse( - status = StatusCodes.OK, - entity = HttpEntity( - contentType, - teiResponse.toXML + } yield featureFactoryConfig.addHeaderToHttpResponse( + HttpResponse( + status = StatusCodes.OK, + entity = HttpEntity( + contentType, + teiResponse.toXML + ) ) ) @@ -296,19 +305,21 @@ object RouteUtilV2 { /** * Sends a message (resulting from a [[Future]]) to a responder and completes the HTTP request by returning the response as RDF. * - * @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. - * @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. + * @param requestMessageF a [[Future]] containing a [[KnoraRequestV2]] message that should be sent to the responder manager. + * @param requestContext the akka-http [[RequestContext]]. + * @param featureFactoryConfig the per-request feature factory configuration. + * @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. + * @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]]. */ def runRdfRouteWithFuture(requestMessageF: Future[KnoraRequestV2], requestContext: RequestContext, + featureFactoryConfig: FeatureFactoryConfig, settings: KnoraSettingsImpl, responderManager: ActorRef, log: LoggingAdapter, @@ -320,6 +331,7 @@ object RouteUtilV2 { routeResult <- runRdfRoute( requestMessage = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -333,7 +345,7 @@ object RouteUtilV2 { /** * Parses a request entity to a [[jena.graph.Graph]]. * - * @param entityStr the request entity. + * @param entityStr the request entity. * @param requestContext the request context. * @return the corresponding [[jena.graph.Graph]]. */ @@ -347,7 +359,7 @@ object RouteUtilV2 { /** * Parses a request entity to a [[JsonLDDocument]]. * - * @param entityStr the request entity. + * @param entityStr the request entity. * @param requestContext the request context. * @return the corresponding [[JsonLDDocument]]. */ diff --git a/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala index b2d167d6b4..0eb4ae85d2 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala @@ -24,6 +24,7 @@ import com.github.swagger.akka.SwaggerHttpService import com.github.swagger.akka.model.Info import io.swagger.models.auth.BasicAuthDefinition import io.swagger.models.{ExternalDocs, Scheme} +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.routing.admin._ /** @@ -55,14 +56,14 @@ class SwaggerApiDocsRoute(routeData: KnoraRouteData) extends KnoraRoute(routeDat 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(version = "1.8.0") //provides license and other description details - override val externalDocs = Some(new ExternalDocs("Knora Docs", "http://docs.knora.org")) + 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 knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { routes } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/VersionRoute.scala b/webapi/src/main/scala/org/knora/webapi/routing/VersionRoute.scala index e828e750c7..f7f65b4cf6 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/VersionRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/VersionRoute.scala @@ -23,6 +23,7 @@ import akka.http.scaladsl.model._ import akka.http.scaladsl.server.Directives.{get, path} import akka.http.scaladsl.server.Route import akka.util.Timeout +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.http.version.versioninfo.VersionInfo import spray.json.{JsObject, JsString} @@ -44,9 +45,8 @@ trait VersionCheck { override implicit val timeout: Timeout = 1.second - protected def versionCheck() = { - val result = getVersion() - createResponse(result) + protected def versionCheck: HttpResponse = { + createResponse(getVersion) } protected def createResponse(result: VersionCheckResult): HttpResponse = { @@ -66,7 +66,7 @@ trait VersionCheck { ) } - private def getVersion() = { + private def getVersion: VersionCheckResult = { var sipiVersion = VersionInfo.sipiVersion val sipiIndex = sipiVersion.indexOf(':') sipiVersion = if (sipiIndex > 0) sipiVersion.substring(sipiIndex + 1) else sipiVersion @@ -94,11 +94,10 @@ class VersionRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("version") { get { - requestContext => - requestContext.complete(versionCheck()) + requestContext => requestContext.complete(versionCheck) } } } 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 962ea93623..55ba181fda 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 @@ -26,6 +26,7 @@ import akka.http.scaladsl.server.{PathMatcher, Route} import io.swagger.annotations._ import javax.ws.rs.Path import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.groupsmessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} @@ -35,7 +36,7 @@ object GroupsRouteADM { } /** - * Provides a spray-routing function for API routes that deal with groups. + * Provides a routing function for API routes that deal with groups. */ @Api(value = "groups", produces = "application/json") @@ -43,11 +44,20 @@ object GroupsRouteADM { class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator with GroupsADMJsonProtocol { import GroupsRouteADM._ - + + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getGroups(featureFactoryConfig) ~ + createGroup(featureFactoryConfig) ~ + getGroupByIri(featureFactoryConfig) ~ + updateGroup(featureFactoryConfig) ~ + changeGroupStatus(featureFactoryConfig) ~ + deleteGroup(featureFactoryConfig) ~ + getGroupMembers(featureFactoryConfig) + /** * Returns all groups */ - private def getGroups: Route = path(GroupsBasePath) { + private def getGroups(featureFactoryConfig: FeatureFactoryConfig): Route = path(GroupsBasePath) { get { /* return all groups */ requestContext => @@ -56,11 +66,12 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi } yield GroupsGetRequestADM(requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -68,7 +79,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi /** * Creates a group */ - private def createGroup: Route = path(GroupsBasePath) { + private def createGroup(featureFactoryConfig: FeatureFactoryConfig): Route = path(GroupsBasePath) { post { /* create a new group */ entity(as[CreateGroupApiRequestADM]) { apiRequest => @@ -82,11 +93,12 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -95,7 +107,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi /** * Returns a single group identified by IRI. */ - private def getGroupByIri: Route = path(GroupsBasePath / Segment) { value => + private def getGroupByIri(featureFactoryConfig: FeatureFactoryConfig): Route = path(GroupsBasePath / Segment) { value => get { /* returns a single group identified through iri */ requestContext => @@ -106,11 +118,12 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi } yield GroupGetRequestADM(checkedGroupIri, requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -118,7 +131,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi /** * Update basic group information. */ - private def updateGroup: Route = path(GroupsBasePath / Segment) { value => + private def updateGroup(featureFactoryConfig: FeatureFactoryConfig): Route = path(GroupsBasePath / Segment) { value => put { /* update a group identified by iri */ entity(as[ChangeGroupApiRequestADM]) { apiRequest => @@ -144,11 +157,12 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -157,7 +171,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi /** * Update the group's status. */ - private def changeGroupStatus: Route = path(GroupsBasePath / Segment / "status") { value => + private def changeGroupStatus(featureFactoryConfig: FeatureFactoryConfig): Route = path(GroupsBasePath / Segment / "status") { value => put { /* change the status of a group identified by iri */ entity(as[ChangeGroupApiRequestADM]) { apiRequest => @@ -186,11 +200,12 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -199,7 +214,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi /** * Deletes a group (sets status to false) */ - private def deleteGroup: Route = path(GroupsBasePath / Segment) { value => + private def deleteGroup(featureFactoryConfig: FeatureFactoryConfig): Route = path(GroupsBasePath / Segment) { value => delete { /* update group status to false */ requestContext => @@ -215,11 +230,12 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -227,7 +243,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi /** * Gets members of single group. */ - private def getGroupMembers: Route = path(GroupsBasePath / Segment / "members") { value => + private def getGroupMembers(featureFactoryConfig: FeatureFactoryConfig): Route = path(GroupsBasePath / Segment / "members") { value => get { /* returns all members of the group identified through iri */ requestContext => @@ -238,19 +254,13 @@ class GroupsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wi } yield GroupMembersGetRequestADM(groupIri = checkedGroupIri, requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } - - /** - * Returns the route. - */ - override def knoraApiPath: Route = getGroups ~ createGroup ~ getGroupByIri ~ - updateGroup ~ changeGroupStatus ~ deleteGroup ~ getGroupMembers - } 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 e1901995f0..b8da3ccfed 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 @@ -19,260 +19,22 @@ package org.knora.webapi.routing.admin -import java.util.UUID - -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{PathMatcher, Route} +import akka.http.scaladsl.server.Route import io.swagger.annotations._ import javax.ws.rs.Path -import org.knora.webapi._ -import org.knora.webapi.exceptions.{BadRequestException, NotImplementedException} -import org.knora.webapi.messages.admin.responder.listsmessages._ -import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} - -import scala.concurrent.Future - -object ListsRouteADM { - val ListsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") -} +import org.knora.webapi.feature.FeatureFactoryConfig +import org.knora.webapi.routing.admin.lists.ListsRouteADMFeatureFactory +import org.knora.webapi.routing.{KnoraRoute, KnoraRouteData} /** * Provides an akka-http-routing function for API routes that deal with lists. */ @Api(value = "lists", produces = "application/json") @Path("/admin/lists") -class ListsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator with ListADMJsonProtocol { - - import ListsRouteADM._ - - /** - * Returns the route. - */ - - override def knoraApiPath: Route = getLists ~ createList ~ getList ~ updateList ~ createListChildNode ~ deleteListNode ~ - getListInfo ~ getListNodeInfo - - /* return all lists optionally filtered by project */ - @ApiOperation(value = "Get lists", nickname = "getlists", httpMethod = "GET", response = classOf[ListsGetResponseADM]) - @ApiResponses(Array( - new ApiResponse(code = 500, message = "Internal server error") - )) - /* return all lists optionally filtered by project */ - def getLists: Route = path(ListsBasePath) { - get { - /* return all lists */ - parameters("projectIri".?) { maybeProjectIri: Option[IRI] => - requestContext => - val projectIri = stringFormatter.toOptionalIri(maybeProjectIri, throw BadRequestException(s"Invalid param project IRI: $maybeProjectIri")) - - val requestMessage: Future[ListsGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListsGetRequestADM(projectIri, requestingUser) - - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } - } - } - - /* create a new list (root node) */ - @ApiOperation(value = "Add new list", nickname = "addList", httpMethod = "POST", response = classOf[ListGetResponseADM]) - @ApiImplicitParams(Array( - new ApiImplicitParam(name = "body", value = "\"list\" to create", required = true, - dataTypeClass = classOf[CreateListApiRequestADM], paramType = "body") - )) - @ApiResponses(Array( - new ApiResponse(code = 500, message = "Internal server error") - )) - def createList: Route = path(ListsBasePath) { - post { - /* create a list */ - entity(as[CreateListApiRequestADM]) { apiRequest => - requestContext => - val requestMessage: Future[ListCreateRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListCreateRequestADM( - createListRequest = apiRequest, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) - - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } - } - } - - /* get a list */ - @Path("/{IRI}") - @ApiOperation(value = "Get a list", nickname = "getlist", httpMethod = "GET", response = classOf[ListGetResponseADM]) - @ApiResponses(Array( - new ApiResponse(code = 500, message = "Internal server error") - )) - def getList: Route = path(ListsBasePath / Segment) { iri => - get { - /* return a list (a graph with all list nodes) */ - requestContext => - val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - - val requestMessage: Future[ListGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListGetRequestADM(listIri, requestingUser) - - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } - } - - /** - * update list - */ - @Path("/{IRI}") - @ApiOperation(value = "Update basic list information", nickname = "putList", httpMethod = "PUT", response = classOf[ListInfoGetResponseADM]) - @ApiImplicitParams(Array( - new ApiImplicitParam(name = "body", value = "\"list\" to update", required = true, - dataTypeClass = classOf[ChangeListInfoApiRequestADM], paramType = "body") - )) - @ApiResponses(Array( - new ApiResponse(code = 500, message = "Internal server error") - )) - def updateList: Route = path(ListsBasePath / Segment) { iri => - put { - /* update existing list node (either root or child) */ - entity(as[ChangeListInfoApiRequestADM]) { apiRequest => - requestContext => - val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - - val requestMessage: Future[ListInfoChangeRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListInfoChangeRequestADM( - listIri = listIri, - changeListRequest = apiRequest, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) - - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } - } - } - - /** - * create a new child node - */ - @Path("/{IRI}") - @ApiOperation(value = "Add new child node", nickname = "addListChildNode", httpMethod = "POST", response = classOf[ListNodeInfoGetResponseADM]) - @ApiImplicitParams(Array( - new ApiImplicitParam(name = "body", value = "\"node\" to create", required = true, - dataTypeClass = classOf[CreateChildNodeApiRequestADM], paramType = "body") - )) - @ApiResponses(Array( - new ApiResponse(code = 500, message = "Internal server error") - )) - def createListChildNode: Route = path(ListsBasePath / Segment) { iri => - post { - /* add node to existing list node. the existing list node can be either the root or a child */ - entity(as[CreateChildNodeApiRequestADM]) { apiRequest => - requestContext => - val parentNodeIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - - val requestMessage: Future[ListChildNodeCreateRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListChildNodeCreateRequestADM( - parentNodeIri = parentNodeIri, - createChildNodeRequest = apiRequest, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) - - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } - } - } - - /* delete list node which should also delete its children */ - def deleteListNode: Route = path(ListsBasePath / Segment) { iri => - delete { - /* delete (deactivate) list */ - throw NotImplementedException("Method not implemented.") - ??? - } - } - - def getListInfo: Route = path(ListsBasePath / "infos" / Segment) { iri => - get { - /* return information about a list (without children) */ - requestContext => - val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - - val requestMessage: Future[ListInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListInfoGetRequestADM(listIri, requestingUser) - - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } - } - - def getListNodeInfo: Route = path(ListsBasePath / "nodes" / Segment) { iri => - get { - /* return information about a single node (without children) */ - requestContext => - val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - - val requestMessage: Future[ListNodeInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListNodeInfoGetRequestADM(listIri, requestingUser) +class ListsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) { + private val featureFactory: ListsRouteADMFeatureFactory = new ListsRouteADMFeatureFactory(routeData) - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } ~ - put { - /* update list node */ - throw NotImplementedException("Method not implemented.") - ??? - } ~ - delete { - /* delete list node */ - throw NotImplementedException("Method not implemented.") - ??? - } + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + featureFactory.makeRoute(featureFactoryConfig) } } 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 dd79e0baca..27a5900e6e 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 @@ -25,6 +25,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import io.swagger.annotations._ import javax.ws.rs.Path +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.permissionsmessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} @@ -45,16 +46,16 @@ class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeDat /** * Returns the route. */ - override def knoraApiPath: Route = - getAdministrativePermissionForProjectGroup ~ - getAdministrativePermissionsForProject ~ - getDefaultObjectAccessPermissionsForProject ~ - getPermissionsForProject ~ - createAdministrativePermission ~ - createDefaultObjectAccessPermission + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getAdministrativePermissionForProjectGroup(featureFactoryConfig) ~ + getAdministrativePermissionsForProject(featureFactoryConfig) ~ + getDefaultObjectAccessPermissionsForProject(featureFactoryConfig) ~ + getPermissionsForProject(featureFactoryConfig) ~ + createAdministrativePermission(featureFactoryConfig) ~ + createDefaultObjectAccessPermission(featureFactoryConfig) - private def getAdministrativePermissionForProjectGroup: Route = path(PermissionsBasePath / "ap" / Segment / Segment) { + private def getAdministrativePermissionForProjectGroup(featureFactoryConfig: FeatureFactoryConfig): Route = path(PermissionsBasePath / "ap" / Segment / Segment) { (projectIri, groupIri) => get { requestContext => @@ -63,16 +64,17 @@ class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeDat } yield AdministrativePermissionForProjectGroupGetRequestADM(projectIri, groupIri, requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } - private def getAdministrativePermissionsForProject: Route = path(PermissionsBasePath / "ap" / Segment) { projectIri => + private def getAdministrativePermissionsForProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(PermissionsBasePath / "ap" / Segment) { projectIri => get { requestContext => val requestMessage = for { @@ -84,16 +86,17 @@ class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeDat ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } - private def getDefaultObjectAccessPermissionsForProject: Route = path(PermissionsBasePath / "doap" / Segment) { projectIri => + private def getDefaultObjectAccessPermissionsForProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(PermissionsBasePath / "doap" / Segment) { projectIri => get { requestContext => val requestMessage = for { @@ -105,17 +108,18 @@ class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeDat ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } - private def getPermissionsForProject: Route = path(PermissionsBasePath / Segment) { - (projectIri) => + private def getPermissionsForProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(PermissionsBasePath / Segment) { + projectIri => get { requestContext => val requestMessage = for { @@ -127,11 +131,12 @@ class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeDat ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -139,34 +144,35 @@ class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeDat /** * Create a new administrative permission */ - private def createAdministrativePermission: Route = path(PermissionsBasePath / "ap") { + private def createAdministrativePermission(featureFactoryConfig: FeatureFactoryConfig): Route = path(PermissionsBasePath / "ap") { post { /* create a new administrative permission */ - entity(as[CreateAdministrativePermissionAPIRequestADM]) { apiRequest => - requestContext => - val requestMessage = for { - requestingUser <- getUserADM(requestContext) - } yield AdministrativePermissionCreateRequestADM( - createRequest = apiRequest, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) - - RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log - ) - } + entity(as[CreateAdministrativePermissionAPIRequestADM]) { apiRequest => + requestContext => + val requestMessage = for { + requestingUser <- getUserADM(requestContext) + } yield AdministrativePermissionCreateRequestADM( + createRequest = apiRequest, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } } } /** * Create default object access permission */ - private def createDefaultObjectAccessPermission: Route = path(PermissionsBasePath / "doap") { + private def createDefaultObjectAccessPermission(featureFactoryConfig: FeatureFactoryConfig): Route = path(PermissionsBasePath / "doap") { post { /* create a new default object access permission */ entity(as[CreateDefaultObjectAccessPermissionAPIRequestADM]) { apiRequest => @@ -180,11 +186,12 @@ class PermissionsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeDat ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } 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 9c3f39d7e1..94628d04fb 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 @@ -36,6 +36,7 @@ import javax.ws.rs.Path import org.knora.webapi.IRI import org.knora.webapi.annotation.ApiMayChange import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} @@ -55,18 +56,26 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * Returns the route. */ - override def knoraApiPath: Route = - getProjects ~ - addProject ~ - getKeywords ~ - getProjectKeywords ~ - getProjectByIri ~ getProjectByShortname ~ getProjectByShortcode ~ - changeProject ~ - deleteProject ~ - getProjectMembersByIri ~ getProjectMembersByShortname ~ getProjectMembersByShortcode ~ - getProjectAdminMembersByIri ~ getProjectAdminMembersByShortname ~ getProjectAdminMembersByShortcode ~ - getProjectRestrictedViewSettingsByIri ~ getProjectRestrictedViewSettingsByShortname ~ - getProjectRestrictedViewSettingsByShortcode ~ getProjectData + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getProjects(featureFactoryConfig) ~ + addProject(featureFactoryConfig) ~ + getKeywords(featureFactoryConfig) ~ + getProjectKeywords(featureFactoryConfig) ~ + getProjectByIri(featureFactoryConfig) ~ + getProjectByShortname(featureFactoryConfig) ~ + getProjectByShortcode(featureFactoryConfig) ~ + changeProject(featureFactoryConfig) ~ + deleteProject(featureFactoryConfig) ~ + getProjectMembersByIri(featureFactoryConfig) ~ + getProjectMembersByShortname(featureFactoryConfig) ~ + getProjectMembersByShortcode(featureFactoryConfig) ~ + getProjectAdminMembersByIri(featureFactoryConfig) ~ + getProjectAdminMembersByShortname(featureFactoryConfig) ~ + getProjectAdminMembersByShortcode(featureFactoryConfig) ~ + getProjectRestrictedViewSettingsByIri(featureFactoryConfig) ~ + getProjectRestrictedViewSettingsByShortname(featureFactoryConfig) ~ + getProjectRestrictedViewSettingsByShortcode(featureFactoryConfig) ~ + getProjectData(featureFactoryConfig) /* return all projects */ @@ -74,17 +83,19 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) @ApiResponses(Array( new ApiResponse(code = 500, message = "Internal server error") )) - private def getProjects: Route = path(ProjectsBasePath) { + private def getProjects(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath) { get { requestContext => val requestMessage: Future[ProjectsGetRequestADM] = for { requestingUser <- getUserADM(requestContext) } yield ProjectsGetRequestADM(requestingUser = requestingUser) + RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -98,7 +109,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) @ApiResponses(Array( new ApiResponse(code = 500, message = "Internal server error") )) - private def addProject: Route = path(ProjectsBasePath) { + private def addProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath) { post { entity(as[CreateProjectApiRequestADM]) { apiRequest => requestContext => @@ -111,18 +122,19 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } } /* returns all unique keywords for all projects as a list */ - private def getKeywords: Route = path(ProjectsBasePath / "Keywords") { + private def getKeywords(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "Keywords") { get { requestContext => val requestMessage: Future[ProjectsKeywordsGetRequestADM] = for { @@ -130,17 +142,18 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectsKeywordsGetRequestADM(requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } /* returns all keywords for a single project */ - private def getProjectKeywords: Route = path(ProjectsBasePath / "iri" / Segment / "Keywords") { value => + private def getProjectKeywords(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment / "Keywords") { value => get { requestContext => val checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -150,11 +163,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectKeywordsGetRequestADM(projectIri = checkedProjectIri, requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -162,7 +176,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * returns a single project identified through iri */ - private def getProjectByIri: Route = path(ProjectsBasePath / "iri" / Segment) { value => + private def getProjectByIri(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment) { value => get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { @@ -172,11 +186,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectGetRequestADM(ProjectIdentifierADM(maybeIri = Some(checkedProjectIri)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -184,7 +199,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * returns a single project identified through shortname. */ - private def getProjectByShortname: Route = path(ProjectsBasePath / "shortname" / Segment) { value => + private def getProjectByShortname(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortname" / Segment) { value => get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { @@ -194,11 +209,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectGetRequestADM(ProjectIdentifierADM(maybeShortname = Some(shortNameDec)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -206,7 +222,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * returns a single project identified through shortcode. */ - private def getProjectByShortcode: Route = path(ProjectsBasePath / "shortcode" / Segment) { value => + private def getProjectByShortcode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortcode" / Segment) { value => get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { @@ -216,11 +232,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectGetRequestADM(ProjectIdentifierADM(maybeShortcode = Some(checkedShortcode)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -228,7 +245,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * update a project identified by iri */ - private def changeProject: Route = path(ProjectsBasePath / "iri" / Segment) { value => + private def changeProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment) { value => put { entity(as[ChangeProjectApiRequestADM]) { apiRequest => requestContext => @@ -246,11 +263,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -260,7 +278,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * API MAY CHANGE: update project status to false */ @ApiMayChange - private def deleteProject: Route = path(ProjectsBasePath / "iri" / Segment) { value => + private def deleteProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment) { value => delete { requestContext => val checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -275,11 +293,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -288,7 +307,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * API MAY CHANGE: returns all members part of a project identified through iri */ @ApiMayChange - private def getProjectMembersByIri: Route = path(ProjectsBasePath / "iri" / Segment / "members") { value => + private def getProjectMembersByIri(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment / "members") { value => get { requestContext => @@ -299,11 +318,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectMembersGetRequestADM(ProjectIdentifierADM(maybeIri = Some(checkedProjectIri)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -312,7 +332,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * API MAY CHANGE: returns all members part of a project identified through shortname */ @ApiMayChange - private def getProjectMembersByShortname: Route = path(ProjectsBasePath / "shortname" / Segment / "members") { value => + private def getProjectMembersByShortname(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortname" / Segment / "members") { value => get { requestContext => val requestMessage: Future[ProjectMembersGetRequestADM] = for { @@ -322,11 +342,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectMembersGetRequestADM(ProjectIdentifierADM(maybeShortname = Some(shortNameDec)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -335,7 +356,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * API MAY CHANGE: returns all members part of a project identified through shortcode */ @ApiMayChange - private def getProjectMembersByShortcode: Route = path(ProjectsBasePath / "shortcode" / Segment / "members") { value => + private def getProjectMembersByShortcode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortcode" / Segment / "members") { value => get { requestContext => @@ -346,11 +367,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectMembersGetRequestADM(ProjectIdentifierADM(maybeShortcode = Some(checkedShortcode)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -359,7 +381,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * API MAY CHANGE: returns all admin members part of a project identified through iri */ @ApiMayChange - private def getProjectAdminMembersByIri: Route = path(ProjectsBasePath / "iri" / Segment / "admin-members") { value => + private def getProjectAdminMembersByIri(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment / "admin-members") { value => get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { @@ -369,11 +391,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectAdminMembersGetRequestADM(ProjectIdentifierADM(maybeIri = Some(checkedProjectIri)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -382,7 +405,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * API MAY CHANGE: returns all admin members part of a project identified through shortname */ @ApiMayChange - private def getProjectAdminMembersByShortname: Route = path(ProjectsBasePath / "shortname" / Segment / "admin-members") { value => + private def getProjectAdminMembersByShortname(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortname" / Segment / "admin-members") { value => get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { @@ -392,11 +415,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectAdminMembersGetRequestADM(ProjectIdentifierADM(maybeShortname = Some(checkedShortname)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -405,7 +429,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * API MAY CHANGE: returns all admin members part of a project identified through shortcode */ @ApiMayChange - private def getProjectAdminMembersByShortcode: Route = path(ProjectsBasePath / "shortcode" / Segment / "admin-members") { value => + private def getProjectAdminMembersByShortcode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortcode" / Segment / "admin-members") { value => get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { @@ -415,11 +439,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectAdminMembersGetRequestADM(ProjectIdentifierADM(maybeShortcode = Some(checkedShortcode)), requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -428,7 +453,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * Returns the project's restricted view settings identified through IRI. */ @ApiMayChange - private def getProjectRestrictedViewSettingsByIri: Route = path(ProjectsBasePath / "iri" / Segment / "RestrictedViewSettings") { value: String => + private def getProjectRestrictedViewSettingsByIri(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment / "RestrictedViewSettings") { value: String => get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { @@ -437,11 +462,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectRestrictedViewSettingsGetRequestADM(ProjectIdentifierADM(maybeIri = Some(value)), requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -450,7 +476,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * Returns the project's restricted view settings identified through shortname. */ @ApiMayChange - private def getProjectRestrictedViewSettingsByShortname: Route = path(ProjectsBasePath / "shortname" / Segment / "RestrictedViewSettings") { value: String => + private def getProjectRestrictedViewSettingsByShortname(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortname" / Segment / "RestrictedViewSettings") { value: String => get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { @@ -460,11 +486,12 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectRestrictedViewSettingsGetRequestADM(ProjectIdentifierADM(maybeShortname = Some(shortNameDec)), requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -473,7 +500,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) * Returns the project's restricted view settings identified through shortcode. */ @ApiMayChange - private def getProjectRestrictedViewSettingsByShortcode: Route = path(ProjectsBasePath / "shortcode" / Segment / "RestrictedViewSettings") { value: String => + private def getProjectRestrictedViewSettingsByShortcode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "shortcode" / Segment / "RestrictedViewSettings") { value: String => get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { @@ -481,41 +508,57 @@ class ProjectsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) } yield ProjectRestrictedViewSettingsGetRequestADM(ProjectIdentifierADM(maybeShortcode = Some(value)), requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } + private val projectDataHeader = `Content-Disposition`(ContentDispositionTypes.attachment, Map(("filename", "project-data.trig"))) + /** * Returns all ontologies, data, and configuration belonging to a project. */ - private def getProjectData: Route = path(ProjectsBasePath / "iri" / Segment / "AllData") { projectIri: IRI => + private def getProjectData(featureFactoryConfig: FeatureFactoryConfig): Route = path(ProjectsBasePath / "iri" / Segment / "AllData") { projectIri: IRI => get { - respondWithHeader(`Content-Disposition`(ContentDispositionTypes.attachment, Map(("filename", "project-data.trig")))) { - requestContext => - val projectIdentifier = ProjectIdentifierADM(maybeIri = Some(projectIri)) + featureFactoryConfig.makeHttpResponseHeader match { + case Some(featureToggleHeader) => + respondWithHeaders(projectDataHeader, featureToggleHeader) { + getProjectDataEntity(projectIri) + } + + case None => + respondWithHeaders(projectDataHeader) { + getProjectDataEntity(projectIri) + } + } - val httpEntityFuture: Future[HttpEntity.Chunked] = for { - requestingUser <- getUserADM(requestContext) - requestMessage = ProjectDataGetRequestADM(projectIdentifier, requestingUser) - responseMessage <- (responderManager ? requestMessage).mapTo[ProjectDataGetResponseADM] + } + } - // Stream the output file back to the client, then delete the file. + private def getProjectDataEntity(projectIri: IRI): Route = { + requestContext => + val projectIdentifier = ProjectIdentifierADM(maybeIri = Some(projectIri)) - source: Source[ByteString, Unit] = FileIO.fromPath(responseMessage.projectDataFile.toPath).watchTermination() { - case (_: Future[IOResult], result: Future[Done]) => - result.onComplete((_: Try[Done]) => responseMessage.projectDataFile.delete) - } + val httpEntityFuture: Future[HttpEntity.Chunked] = for { + requestingUser <- getUserADM(requestContext) + requestMessage = ProjectDataGetRequestADM(projectIdentifier, requestingUser) + responseMessage <- (responderManager ? requestMessage).mapTo[ProjectDataGetResponseADM] - httpEntity = HttpEntity(ContentTypes.`application/octet-stream`, source) - } yield httpEntity + // Stream the output file back to the client, then delete the file. - requestContext.complete(httpEntityFuture) - } - } + source: Source[ByteString, Unit] = FileIO.fromPath(responseMessage.projectDataFile.toPath).watchTermination() { + case (_: Future[IOResult], result: Future[Done]) => + result.onComplete((_: Try[Done]) => responseMessage.projectDataFile.delete) + } + + httpEntity = HttpEntity(ContentTypes.`application/octet-stream`, source) + } yield httpEntity + + requestContext.complete(httpEntityFuture) } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/SipiRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/SipiRouteADM.scala index 229f1a6a18..d5f3fd79d2 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/SipiRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/SipiRouteADM.scala @@ -22,8 +22,9 @@ package org.knora.webapi.routing.admin import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.sipimessages.SipiFileInfoGetRequestADM -import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} +import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} /** * Provides a routing function for the API that Sipi connects to. @@ -36,7 +37,7 @@ class SipiRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("admin" / "files" / Segments(2)) { projectIDAndFile: Seq[String] => get { @@ -47,12 +48,13 @@ class SipiRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with filename = stringFormatter.toSparqlEncodedString(projectIDAndFile(1), throw BadRequestException(s"Invalid filename: '${projectIDAndFile(1)}'")) } yield SipiFileInfoGetRequestADM(projectID = projectID, filename = filename, requestingUser = requestingUser) - RouteUtilV1.runJsonRouteWithFuture( - requestMessage, - requestContext, - settings, - responderManager, - log + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } 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 796b7410e9..69dc294793 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 @@ -23,6 +23,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import io.swagger.annotations.Api import javax.ws.rs.Path +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.storesmessages.{ResetTriplestoreContentRequestADM, StoresADMJsonProtocol} import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} @@ -41,7 +42,7 @@ class StoreRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * Returns the route. */ - override def knoraApiPath: Route = Route { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = Route { path("admin" / "store") { get { requestContext => @@ -62,11 +63,12 @@ class StoreRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit val requestMessage = Future.successful(msg) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + 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 b239765367..aa918178e1 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 @@ -27,6 +27,7 @@ import io.swagger.annotations._ import javax.ws.rs.Path import org.knora.webapi.annotation.ApiMayChange import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.usersmessages.UsersADMJsonProtocol._ import org.knora.webapi.messages.admin.responder.usersmessages._ import org.knora.webapi.messages.util.KnoraSystemInstances @@ -51,14 +52,26 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * Returns the route. */ - override def knoraApiPath: Route = - getUsers ~ - addUser ~ getUserByIri ~ getUserByEmail ~ getUserByUsername ~ - changeUserBasicInformation ~ changeUserPassword ~ changeUserStatus ~ deleteUser ~ - changeUserSytemAdminMembership ~ - getUsersProjectMemberships ~ addUserToProjectMembership ~ removeUserFromProjectMembership ~ - getUsersProjectAdminMemberships ~ addUserToProjectAdminMembership ~ removeUserFromProjectAdminMembership ~ - getUsersGroupMemberships ~ addUserToGroupMembership ~ removeUserFromGroupMembership + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getUsers(featureFactoryConfig) ~ + addUser(featureFactoryConfig) ~ + getUserByIri(featureFactoryConfig) ~ + getUserByEmail(featureFactoryConfig) ~ + getUserByUsername(featureFactoryConfig) ~ + changeUserBasicInformation(featureFactoryConfig) ~ + changeUserPassword(featureFactoryConfig) ~ + changeUserStatus(featureFactoryConfig) ~ + deleteUser(featureFactoryConfig) ~ + changeUserSytemAdminMembership(featureFactoryConfig) ~ + getUsersProjectMemberships(featureFactoryConfig) ~ + addUserToProjectMembership(featureFactoryConfig) ~ + removeUserFromProjectMembership(featureFactoryConfig) ~ + getUsersProjectAdminMemberships(featureFactoryConfig) ~ + addUserToProjectAdminMembership(featureFactoryConfig) ~ + removeUserFromProjectAdminMembership(featureFactoryConfig) ~ + getUsersGroupMemberships(featureFactoryConfig) ~ + addUserToGroupMembership(featureFactoryConfig) ~ + removeUserFromGroupMembership(featureFactoryConfig) @ApiOperation(value = "Get users", nickname = "getUsers", httpMethod = "GET", response = classOf[UsersGetResponseADM]) @@ -66,7 +79,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit new ApiResponse(code = 500, message = "Internal server error") )) /* return all users */ - def getUsers: Route = path(UsersBasePath) { + def getUsers(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath) { get { requestContext => val requestMessage: Future[UsersGetRequestADM] = for { @@ -74,11 +87,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } yield UsersGetRequestADM(requestingUser = requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -92,7 +106,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit new ApiResponse(code = 500, message = "Internal server error") )) /* create a new user */ - def addUser: Route = path(UsersBasePath) { + def addUser(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath) { post { entity(as[CreateUserApiRequestADM]) { apiRequest => requestContext => @@ -105,11 +119,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -118,7 +133,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * return a single user identified by iri */ - private def getUserByIri: Route = path(UsersBasePath / "iri" / Segment) { value => + private def getUserByIri(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment) { value => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { @@ -126,11 +141,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } yield UserGetRequestADM(UserIdentifierADM(maybeIri = Some(value)), UserInformationTypeADM.RESTRICTED, requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -138,7 +154,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * return a single user identified by email */ - private def getUserByEmail: Route = path(UsersBasePath / "email" / Segment) { value => + private def getUserByEmail(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "email" / Segment) { value => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { @@ -146,11 +162,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } yield UserGetRequestADM(UserIdentifierADM(maybeEmail = Some(value)), UserInformationTypeADM.RESTRICTED, requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -158,7 +175,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * return a single user identified by username */ - private def getUserByUsername: Route = path(UsersBasePath / "username" / Segment) { value => + private def getUserByUsername(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "username" / Segment) { value => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { @@ -166,11 +183,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } yield UserGetRequestADM(UserIdentifierADM(maybeUsername = Some(value)), UserInformationTypeADM.RESTRICTED, requestingUser) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -179,7 +197,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: Change existing user's basic information. */ @ApiMayChange - private def changeUserBasicInformation: Route = path(UsersBasePath / "iri" / Segment / "BasicUserInformation") { value => + private def changeUserBasicInformation(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "BasicUserInformation") { value => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => @@ -202,11 +220,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -216,7 +235,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: Change user's password. */ @ApiMayChange - private def changeUserPassword: Route = path(UsersBasePath / "iri" / Segment / "Password") { value => + private def changeUserPassword(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "Password") { value => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => @@ -239,11 +258,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -253,7 +273,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: Change user's status. */ @ApiMayChange - private def changeUserStatus: Route = path(UsersBasePath / "iri" / Segment / "Status") { value => + private def changeUserStatus(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "Status") { value => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => @@ -276,21 +296,22 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } } - + /** * API MAY CHANGE: delete a user identified by iri (change status to false). */ @ApiMayChange - private def deleteUser: Route = path(UsersBasePath / "iri" / Segment) { value => + private def deleteUser(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment) { value => delete { requestContext => { val userIri = stringFormatter.validateAndEscapeUserIri(value, throw BadRequestException(s"Invalid user IRI $value")) @@ -310,11 +331,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -324,7 +346,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: Change user's SystemAdmin membership. */ @ApiMayChange - private def changeUserSytemAdminMembership: Route = path(UsersBasePath / "iri" / Segment / "SystemAdmin") { value => + private def changeUserSytemAdminMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "SystemAdmin") { value => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => @@ -347,11 +369,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -361,7 +384,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: get user's project memberships */ @ApiMayChange - private def getUsersProjectMemberships: Route = path(UsersBasePath / "iri" / Segment / "project-memberships") { userIri => + private def getUsersProjectMemberships(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-memberships") { userIri => get { requestContext => val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) @@ -375,11 +398,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -388,7 +412,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: add user to project */ @ApiMayChange - private def addUserToProjectMembership: Route = path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => + private def addUserToProjectMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => post { requestContext => val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) @@ -404,11 +428,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -417,7 +442,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: remove user from project (and all groups belonging to this project) */ @ApiMayChange - private def removeUserFromProjectMembership: Route = path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => + private def removeUserFromProjectMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => delete { requestContext => @@ -434,11 +459,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -447,7 +473,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: get user's project admin memberships */ @ApiMayChange - private def getUsersProjectAdminMemberships: Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships") { userIri => + private def getUsersProjectAdminMemberships(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships") { userIri => get { requestContext => val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) @@ -461,11 +487,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -474,7 +501,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: add user to project admin */ @ApiMayChange - private def addUserToProjectAdminMembership: Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => + private def addUserToProjectAdminMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => post { /* */ requestContext => @@ -491,11 +518,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -504,7 +532,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: remove user from project admin membership */ @ApiMayChange - private def removeUserFromProjectAdminMembership: Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => + private def removeUserFromProjectAdminMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => delete { requestContext => val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) @@ -520,11 +548,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -533,7 +562,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: get user's group memberships */ @ApiMayChange - private def getUsersGroupMemberships: Route = path(UsersBasePath / "iri" / Segment / "group-memberships") { userIri => + private def getUsersGroupMemberships(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "group-memberships") { userIri => get { requestContext => val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) @@ -547,11 +576,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -560,7 +590,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: add user to group */ @ApiMayChange - private def addUserToGroupMembership: Route = path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => + private def addUserToGroupMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => post { requestContext => val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) @@ -576,11 +606,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } @@ -589,7 +620,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: remove user from group */ @ApiMayChange - private def removeUserFromGroupMembership: Route = path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => + private def removeUserFromGroupMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => delete { requestContext => val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) @@ -605,11 +636,12 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) RouteUtilADM.runJsonRoute( - requestMessage, - requestContext, - settings, - responderManager, - log + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log ) } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/ListsRouteADMFeatureFactory.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/ListsRouteADMFeatureFactory.scala new file mode 100644 index 0000000000..3ddf53856e --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/ListsRouteADMFeatureFactory.scala @@ -0,0 +1,57 @@ +/* + * Copyright © 2015-2019 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi.routing.admin.lists + +import akka.http.scaladsl.server.Route +import org.knora.webapi.feature.{FeatureFactory, FeatureFactoryConfig} +import org.knora.webapi.routing.{KnoraRouteData, KnoraRouteFactory} + +/** + * A [[FeatureFactory]] that constructs list admin routes. + * + * @param routeData the [[KnoraRouteData]] to be used in constructing the routes. + */ +class ListsRouteADMFeatureFactory(routeData: KnoraRouteData) extends KnoraRouteFactory(routeData) + with FeatureFactory { + + /** + * The old lists route feature. + */ + private val oldListsRouteADMFeature = new OldListsRouteADMFeature(routeData) + + /** + * The new lists route feature. + */ + private val newListsRouteADMFeature = new NewListsRouteADMFeature(routeData) + + /** + * Returns a lists route reflecting the specified feature factory configuration. + * + * @param featureFactoryConfig a [[FeatureFactoryConfig]]. + * @return a lists route. + */ + def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + if (featureFactoryConfig.getToggle("new-list-admin-routes").isEnabled) { + newListsRouteADMFeature.makeRoute(featureFactoryConfig) + } else { + oldListsRouteADMFeature.makeRoute(featureFactoryConfig) + } + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/NewListsRouteADMFeature.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/NewListsRouteADMFeature.scala new file mode 100644 index 0000000000..afbcff75e6 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/NewListsRouteADMFeature.scala @@ -0,0 +1,293 @@ +/* + * Copyright © 2015-2019 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi.routing.admin.lists + +import java.util.UUID + +import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCodes} +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.{PathMatcher, Route} +import akka.http.scaladsl.util.FastFuture +import io.swagger.annotations._ +import javax.ws.rs.Path +import org.knora.webapi.IRI +import org.knora.webapi.exceptions.{BadRequestException, NotImplementedException} +import org.knora.webapi.feature.{Feature, FeatureFactoryConfig} +import org.knora.webapi.messages.admin.responder.listsmessages._ +import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} + +import scala.concurrent.Future + +object NewListsRouteADMFeature { + val ListsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") +} + +/** + * A [[Feature]] that provides the new list admin API route. + * + * @param routeData the [[KnoraRouteData]] to be used in constructing the route. + */ +class NewListsRouteADMFeature(routeData: KnoraRouteData) extends KnoraRoute(routeData) + with Feature with Authenticator with ListADMJsonProtocol { + + import NewListsRouteADMFeature._ + + def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getLists(featureFactoryConfig) ~ + createList(featureFactoryConfig) ~ + getListNode(featureFactoryConfig) ~ + updateList(featureFactoryConfig) ~ + createListChildNode(featureFactoryConfig) ~ + deleteListNode(featureFactoryConfig) ~ + getListInfo(featureFactoryConfig) ~ + getListNodeInfo(featureFactoryConfig) + + /* return all lists optionally filtered by project */ + @ApiOperation(value = "Get lists", nickname = "getlists", httpMethod = "GET", response = classOf[ListsGetResponseADM]) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + /* return all lists optionally filtered by project */ + private def getLists(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath) { + get { + /* return all lists */ + parameters("projectIri".?) { maybeProjectIri: Option[IRI] => + requestContext => + val projectIri = stringFormatter.toOptionalIri(maybeProjectIri, throw BadRequestException(s"Invalid param project IRI: $maybeProjectIri")) + + val requestMessage: Future[ListsGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListsGetRequestADM(projectIri, requestingUser) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /* create a new list (root node) */ + @ApiOperation(value = "Add new list", nickname = "addList", httpMethod = "POST", response = classOf[ListGetResponseADM]) + @ApiImplicitParams(Array( + new ApiImplicitParam(name = "body", value = "\"list\" to create", required = true, + dataTypeClass = classOf[CreateListApiRequestADM], paramType = "body") + )) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def createList(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath) { + post { + /* create a list */ + entity(as[CreateListApiRequestADM]) { apiRequest => + requestContext => + val requestMessage: Future[ListCreateRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListCreateRequestADM( + createListRequest = apiRequest, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /* get a list */ + @Path("/{IRI}") + @ApiOperation(value = "Get a list", nickname = "getlist", httpMethod = "GET", response = classOf[ListGetResponseADM]) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def getListNode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + get { + requestContext => + val dummyResponse: String = + """{ "result": "You are using the new list API" }""".stripMargin + + val httpResponse = FastFuture.successful { + featureFactoryConfig.addHeaderToHttpResponse( + HttpResponse( + status = StatusCodes.OK, + entity = HttpEntity( + ContentTypes.`application/json`, + dummyResponse + ) + ) + ) + } + + requestContext.complete(httpResponse) + } + } + + /** + * update list + */ + @Path("/{IRI}") + @ApiOperation(value = "Update basic list information", nickname = "putList", httpMethod = "PUT", response = classOf[ListInfoGetResponseADM]) + @ApiImplicitParams(Array( + new ApiImplicitParam(name = "body", value = "\"list\" to update", required = true, + dataTypeClass = classOf[ChangeListInfoApiRequestADM], paramType = "body") + )) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def updateList(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + put { + /* update existing list node (either root or child) */ + entity(as[ChangeListInfoApiRequestADM]) { apiRequest => + requestContext => + val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListInfoChangeRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListInfoChangeRequestADM( + listIri = listIri, + changeListRequest = apiRequest, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /** + * create a new child node + */ + @Path("/{IRI}") + @ApiOperation(value = "Add new child node", nickname = "addListChildNode", httpMethod = "POST", response = classOf[ListNodeInfoGetResponseADM]) + @ApiImplicitParams(Array( + new ApiImplicitParam(name = "body", value = "\"node\" to create", required = true, + dataTypeClass = classOf[CreateChildNodeApiRequestADM], paramType = "body") + )) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def createListChildNode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + post { + /* add node to existing list node. the existing list node can be either the root or a child */ + entity(as[CreateChildNodeApiRequestADM]) { apiRequest => + requestContext => + val parentNodeIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListChildNodeCreateRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListChildNodeCreateRequestADM( + parentNodeIri = parentNodeIri, + createChildNodeRequest = apiRequest, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /* delete list node which should also delete its children */ + private def deleteListNode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + delete { + /* delete (deactivate) list */ + throw NotImplementedException("Method not implemented.") + ??? + } + } + + private def getListInfo(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / "infos" / Segment) { iri => + get { + /* return information about a list (without children) */ + requestContext => + val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListInfoGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListInfoGetRequestADM(listIri, requestingUser) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + + private def getListNodeInfo(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / "nodes" / Segment) { iri => + get { + /* return information about a single node (without children) */ + requestContext => + val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListNodeInfoGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListNodeInfoGetRequestADM(listIri, requestingUser) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } ~ + put { + /* update list node */ + throw NotImplementedException("Method not implemented.") + ??? + } ~ + delete { + /* delete list node */ + throw NotImplementedException("Method not implemented.") + ??? + } + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala new file mode 100644 index 0000000000..633ab1f385 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala @@ -0,0 +1,290 @@ +/* + * Copyright © 2015-2019 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi.routing.admin.lists + +import java.util.UUID + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.{PathMatcher, Route} +import io.swagger.annotations._ +import javax.ws.rs.Path +import org.knora.webapi.IRI +import org.knora.webapi.exceptions.{BadRequestException, NotImplementedException} +import org.knora.webapi.feature.{Feature, FeatureFactoryConfig} +import org.knora.webapi.messages.admin.responder.listsmessages._ +import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} + +import scala.concurrent.Future + +object OldListsRouteADMFeature { + val ListsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") +} + +/** + * A [[Feature]] that provides the old list admin API route. + * + * @param routeData the [[KnoraRouteData]] to be used in constructing the route. + */ +class OldListsRouteADMFeature(routeData: KnoraRouteData) extends KnoraRoute(routeData) + with Feature with Authenticator with ListADMJsonProtocol { + + import OldListsRouteADMFeature._ + + def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getLists(featureFactoryConfig) ~ + createList(featureFactoryConfig) ~ + getList(featureFactoryConfig) ~ + updateList(featureFactoryConfig) ~ + createListChildNode(featureFactoryConfig) ~ + deleteListNode(featureFactoryConfig) ~ + getListInfo(featureFactoryConfig) ~ + getListNodeInfo(featureFactoryConfig) + + /* return all lists optionally filtered by project */ + @ApiOperation(value = "Get lists", nickname = "getlists", httpMethod = "GET", response = classOf[ListsGetResponseADM]) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + /* return all lists optionally filtered by project */ + private def getLists(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath) { + get { + /* return all lists */ + parameters("projectIri".?) { maybeProjectIri: Option[IRI] => + requestContext => + val projectIri = stringFormatter.toOptionalIri(maybeProjectIri, throw BadRequestException(s"Invalid param project IRI: $maybeProjectIri")) + + val requestMessage: Future[ListsGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListsGetRequestADM(projectIri, requestingUser) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /* create a new list (root node) */ + @ApiOperation(value = "Add new list", nickname = "addList", httpMethod = "POST", response = classOf[ListGetResponseADM]) + @ApiImplicitParams(Array( + new ApiImplicitParam(name = "body", value = "\"list\" to create", required = true, + dataTypeClass = classOf[CreateListApiRequestADM], paramType = "body") + )) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def createList(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath) { + post { + /* create a list */ + entity(as[CreateListApiRequestADM]) { apiRequest => + requestContext => + val requestMessage: Future[ListCreateRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListCreateRequestADM( + createListRequest = apiRequest, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /* get a list */ + @Path("/{IRI}") + @ApiOperation(value = "Get a list", nickname = "getlist", httpMethod = "GET", response = classOf[ListGetResponseADM]) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def getList(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + get { + /* return a list (a graph with all list nodes) */ + requestContext => + val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListGetRequestADM(listIri, requestingUser) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + + /** + * update list + */ + @Path("/{IRI}") + @ApiOperation(value = "Update basic list information", nickname = "putList", httpMethod = "PUT", response = classOf[ListInfoGetResponseADM]) + @ApiImplicitParams(Array( + new ApiImplicitParam(name = "body", value = "\"list\" to update", required = true, + dataTypeClass = classOf[ChangeListInfoApiRequestADM], paramType = "body") + )) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def updateList(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + put { + /* update existing list node (either root or child) */ + entity(as[ChangeListInfoApiRequestADM]) { apiRequest => + requestContext => + val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListInfoChangeRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListInfoChangeRequestADM( + listIri = listIri, + changeListRequest = apiRequest, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /** + * create a new child node + */ + @Path("/{IRI}") + @ApiOperation(value = "Add new child node", nickname = "addListChildNode", httpMethod = "POST", response = classOf[ListNodeInfoGetResponseADM]) + @ApiImplicitParams(Array( + new ApiImplicitParam(name = "body", value = "\"node\" to create", required = true, + dataTypeClass = classOf[CreateChildNodeApiRequestADM], paramType = "body") + )) + @ApiResponses(Array( + new ApiResponse(code = 500, message = "Internal server error") + )) + private def createListChildNode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + post { + /* add node to existing list node. the existing list node can be either the root or a child */ + entity(as[CreateChildNodeApiRequestADM]) { apiRequest => + requestContext => + val parentNodeIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListChildNodeCreateRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListChildNodeCreateRequestADM( + parentNodeIri = parentNodeIri, + createChildNodeRequest = apiRequest, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + } + + /* delete list node which should also delete its children */ + private def deleteListNode(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / Segment) { iri => + delete { + /* delete (deactivate) list */ + throw NotImplementedException("Method not implemented.") + ??? + } + } + + private def getListInfo(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / "infos" / Segment) { iri => + get { + /* return information about a list (without children) */ + requestContext => + val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListInfoGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListInfoGetRequestADM(listIri, requestingUser) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } + } + + private def getListNodeInfo(featureFactoryConfig: FeatureFactoryConfig): Route = path(ListsBasePath / "nodes" / Segment) { iri => + get { + /* return information about a single node (without children) */ + requestContext => + val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListNodeInfoGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListNodeInfoGetRequestADM(listIri, requestingUser) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) + } ~ + put { + /* update list node */ + throw NotImplementedException("Method not implemented.") + ??? + } ~ + delete { + /* delete list node */ + throw NotImplementedException("Method not implemented.") + ??? + } + } +} 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 c06fbdb69d..77156e613c 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 @@ -27,6 +27,7 @@ import akka.http.scaladsl.model.{HttpEntity, HttpResponse, MediaTypes} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import javax.imageio.ImageIO +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData} /** @@ -37,7 +38,7 @@ class AssetsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "assets" / Remaining) { assetId => get { 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 c1d9401db2..b9c9fda54b 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 @@ -21,6 +21,7 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData} /** @@ -31,7 +32,7 @@ class AuthenticationRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeD /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "authenticate") { get { 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 d8bd5be769..d0bcb7b111 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 @@ -21,6 +21,7 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.v1.responder.ckanmessages.CkanRequestV1 import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} @@ -32,7 +33,7 @@ class CkanRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "ckan") { get { 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 e2ee3d405f..f0e1150188 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 @@ -22,6 +22,7 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.v1.responder.listmessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} import org.knora.webapi.messages.StringFormatter @@ -34,7 +35,7 @@ class ListsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { val stringFormatter = StringFormatter.getGeneralInstance 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 7a5d5312a3..0704a5f743 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 @@ -24,6 +24,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.apache.commons.validator.routines.UrlValidator import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.v1.responder.projectmessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} @@ -35,7 +36,7 @@ class ProjectsRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) w /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "projects") { get { 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 07da9b8c4c..592f04b412 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 @@ -22,6 +22,7 @@ package org.knora.webapi.routing.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.v1.responder.ontologymessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} @@ -33,7 +34,7 @@ class ResourceTypesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeDa /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "resourcetypes" / Segment) { iri => get { 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 fde6c622bf..e8a8a2d44a 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 javax.xml.transform.stream.StreamSource import javax.xml.validation.{Schema, SchemaFactory, Validator} import org.knora.webapi._ import org.knora.webapi.exceptions.{AssertionException, BadRequestException, ForbiddenException, InconsistentTriplestoreDataException, SipiException} +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter.XmlImportNamespaceInfoV1 import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectGetRequestADM, ProjectGetResponseADM, ProjectIdentifierADM} @@ -69,7 +70,7 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { def makeResourceRequestMessage(resIri: String, resinfo: Boolean, 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 3a89f2d655..da01c19865 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 @@ -26,6 +26,7 @@ import org.knora.webapi.messages.v1.responder.searchmessages.{ExtendedSearchGetR import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} import org.knora.webapi.IRI import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.StringFormatter import scala.language.postfixOps @@ -184,7 +185,7 @@ class SearchRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "search" /) { // in the original API, there is a slash after "search": "http://www.salsah.org/api/search/?searchtype=extended" 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 99a6e04cd3..ffc9a9335e 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 @@ -26,6 +26,7 @@ import akka.http.scaladsl.model.Multipart.BodyPart import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.v1.responder.standoffmessages.RepresentationV1JsonProtocol.createMappingApiRequestV1Format import org.knora.webapi.messages.v1.responder.standoffmessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} @@ -43,7 +44,7 @@ class StandoffRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) w /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "mapping") { post { 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 feb376ae1a..af3f9c8993 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 @@ -26,6 +26,7 @@ import akka.http.scaladsl.server.Route import org.apache.commons.validator.routines.UrlValidator import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.v1.responder.usermessages._ import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV1} @@ -40,7 +41,7 @@ class UsersRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v1" / "users") { get { 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 64e0bd3778..d0728fed11 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 @@ -28,6 +28,7 @@ import akka.http.scaladsl.util.FastFuture import akka.pattern._ import org.knora.webapi._ import org.knora.webapi.exceptions.{BadRequestException, InconsistentTriplestoreDataException, NotFoundException} +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages.{GetFileMetadataRequest, GetFileMetadataResponse} @@ -48,7 +49,7 @@ class ValuesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { def makeVersionHistoryRequestMessage(iris: Seq[IRI], userADM: UserADM): ValueVersionHistoryGetRequestV1 = { if (iris.length != 3) throw BadRequestException("Version history request requires resource IRI, property IRI, and current value IRI") 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 33e51bdc27..3e9f8c2a11 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 @@ -21,6 +21,7 @@ package org.knora.webapi.routing.v2 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.usersmessages.UserIdentifierADM import org.knora.webapi.messages.v2.routing.authenticationmessages.{AuthenticationV2JsonProtocol, KnoraPasswordCredentialsV2, LoginApiRequestPayloadV2} import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData} @@ -33,7 +34,7 @@ class AuthenticationRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeD /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v2" / "authentication") { get { // authenticate credentials 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 6baee5049b..0e1ed36953 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 @@ -23,6 +23,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.v2.responder.listsmessages.{ListGetRequestV2, NodeGetRequestV2} import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV2} @@ -36,9 +37,11 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with /** * Returns the route. */ - override def knoraApiPath: Route = getList ~ getNode + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getList(featureFactoryConfig) ~ + getNode(featureFactoryConfig) - private def getList: Route = path("v2" / "lists" / Segment) { lIri: String => + private def getList(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "lists" / Segment) { lIri: String => get { /* return a list (a graph with all list nodes) */ requestContext => @@ -50,6 +53,7 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -59,7 +63,7 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with } } - private def getNode: Route = path("v2" / "node" / Segment) { nIri: String => + private def getNode(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "node" / Segment) { nIri: String => get { /* return a list node */ requestContext => @@ -71,6 +75,7 @@ class ListsRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/MetadataRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/MetadataRouteV2.scala index 52b011d205..68b7c63b5a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/MetadataRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/MetadataRouteV2.scala @@ -5,6 +5,7 @@ import java.util.UUID import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import org.apache.jena.graph.Graph +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.{ApiV2Complex, InternalSchema} import org.knora.webapi.messages.v2.responder.metadatamessages.{MetadataGetRequestV2, MetadataPutRequestV2} import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV2} @@ -24,12 +25,14 @@ class MetadataRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w /** * Returns the route. */ - override def knoraApiPath: Route = getMetadata ~ setMetadata + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getMetadata(featureFactoryConfig) ~ + setMetadata(featureFactoryConfig) /** * Route to get metadata. */ - private def getMetadata: Route = path(MetadataBasePath / Segment) { projectIri => + private def getMetadata(featureFactoryConfig: FeatureFactoryConfig): Route = path(MetadataBasePath / Segment) { projectIri => get { requestContext => { // Make the request message. @@ -45,6 +48,7 @@ class MetadataRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -58,7 +62,7 @@ class MetadataRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w /** * Route to set a project's metadata, replacing any existing metadata for the project. */ - private def setMetadata: Route = path(MetadataBasePath / Segment) { projectIri => + private def setMetadata(featureFactoryConfig: FeatureFactoryConfig): Route = path(MetadataBasePath / Segment) { projectIri => put { entity(as[String]) { entityStr => requestContext => { @@ -83,6 +87,7 @@ class MetadataRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, 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 89aaaa7044..73ce39aa59 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 @@ -19,13 +19,13 @@ package org.knora.webapi.routing.v2 -import java.time.Instant import java.util.UUID import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.util.{JsonLDDocument, JsonLDUtil} import org.knora.webapi.messages.v2.responder.ontologymessages._ @@ -51,12 +51,26 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * Returns the route. */ - override def knoraApiPath: Route = dereferenceOntologyIri ~ getOntologyMetadata ~ updateOntologyMetadata ~ getOntologyMetadataForProjects ~ - getOntology ~ createClass ~ updateClass ~ addCardinalities ~ replaceCardinalities ~ getClasses ~ - deleteClass ~ createProperty ~ updateProperty ~ getProperties ~ deleteProperty ~ createOntology ~ - deleteOntology - - private def dereferenceOntologyIri: Route = path("ontology" / Segments) { _: List[String] => + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + dereferenceOntologyIri(featureFactoryConfig) ~ + getOntologyMetadata(featureFactoryConfig) ~ + updateOntologyMetadata(featureFactoryConfig) ~ + getOntologyMetadataForProjects(featureFactoryConfig) ~ + getOntology(featureFactoryConfig) ~ + createClass(featureFactoryConfig) ~ + updateClass(featureFactoryConfig) ~ + addCardinalities(featureFactoryConfig) ~ + replaceCardinalities(featureFactoryConfig) ~ + getClasses(featureFactoryConfig) ~ + deleteClass(featureFactoryConfig) ~ + createProperty(featureFactoryConfig) ~ + updateProperty(featureFactoryConfig) ~ + getProperties(featureFactoryConfig) ~ + deleteProperty(featureFactoryConfig) ~ + createOntology(featureFactoryConfig) ~ + deleteOntology(featureFactoryConfig) + + private def dereferenceOntologyIri(featureFactoryConfig: FeatureFactoryConfig): Route = path("ontology" / Segments) { _: List[String] => get { requestContext => { // This is the route used to dereference an actual ontology IRI. If the URL path looks like it @@ -97,6 +111,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -107,7 +122,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getOntologyMetadata: Route = path(OntologiesBasePath / "metadata") { + private def getOntologyMetadata(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "metadata") { get { requestContext => { val maybeProjectIri: Option[SmartIri] = RouteUtilV2.getProject(requestContext) @@ -119,6 +134,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -129,7 +145,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def updateOntologyMetadata: Route = path(OntologiesBasePath / "metadata") { + private def updateOntologyMetadata(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "metadata") { put { entity(as[String]) { jsonRequest => requestContext => { @@ -152,6 +168,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -163,7 +180,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getOntologyMetadataForProjects: Route = path(OntologiesBasePath / "metadata" / Segments) { projectIris: List[IRI] => + private def getOntologyMetadataForProjects(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "metadata" / Segments) { projectIris: List[IRI] => get { requestContext => { @@ -175,6 +192,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -185,7 +203,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getOntology: Route = path(OntologiesBasePath / "allentities" / Segment) { externalOntologyIriStr: IRI => + private def getOntology(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "allentities" / Segment) { externalOntologyIriStr: IRI => get { requestContext => { val requestedOntologyIri = externalOntologyIriStr.toSmartIriWithErr(throw BadRequestException(s"Invalid ontology IRI: $externalOntologyIriStr")) @@ -210,6 +228,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -220,7 +239,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def createClass: Route = path(OntologiesBasePath / "classes") { + private def createClass(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "classes") { post { // Create a new class. entity(as[String]) { jsonRequest => @@ -243,6 +262,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -254,7 +274,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def updateClass: Route = path(OntologiesBasePath / "classes") { + private def updateClass(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "classes") { put { // Change the labels or comments of a class. entity(as[String]) { jsonRequest => @@ -277,6 +297,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -288,7 +309,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def addCardinalities: Route = path(OntologiesBasePath / "cardinalities") { + private def addCardinalities(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "cardinalities") { post { // Add cardinalities to a class. entity(as[String]) { jsonRequest => @@ -311,6 +332,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -322,7 +344,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def replaceCardinalities: Route = path(OntologiesBasePath / "cardinalities") { + private def replaceCardinalities(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "cardinalities") { put { // Change a class's cardinalities. entity(as[String]) { jsonRequest => @@ -345,6 +367,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -356,7 +379,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getClasses: Route = path(OntologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => + private def getClasses(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => get { requestContext => { @@ -405,6 +428,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -415,7 +439,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def deleteClass: Route = path(OntologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => + private def deleteClass(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => delete { requestContext => { @@ -445,6 +469,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -455,7 +480,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def createProperty: Route = path(OntologiesBasePath / "properties") { + private def createProperty(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "properties") { post { // Create a new property. entity(as[String]) { jsonRequest => @@ -478,6 +503,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -489,7 +515,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def updateProperty: Route = path(OntologiesBasePath / "properties") { + private def updateProperty(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "properties") { put { // Change the labels or comments of a property. entity(as[String]) { jsonRequest => @@ -512,6 +538,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -523,7 +550,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getProperties: Route = path(OntologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => + private def getProperties(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => get { requestContext => { @@ -572,6 +599,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -582,7 +610,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def deleteProperty: Route = path(OntologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => + private def deleteProperty(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => delete { requestContext => { @@ -612,6 +640,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -622,7 +651,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def createOntology: Route = path(OntologiesBasePath) { + private def createOntology(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath) { // Create a new, empty ontology. post { entity(as[String]) { jsonRequest => @@ -645,6 +674,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -656,7 +686,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def deleteOntology: Route = path(OntologiesBasePath / Segment) { ontologyIriStr => + private def deleteOntology(featureFactoryConfig: FeatureFactoryConfig): Route = path(OntologiesBasePath / Segment) { ontologyIriStr => delete { requestContext => { @@ -681,6 +711,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, 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 2da4650acf..8771f6fdbe 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 @@ -26,6 +26,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.util.{JsonLDDocument, JsonLDUtil} import org.knora.webapi.messages.v2.responder.resourcemessages._ @@ -60,11 +61,19 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) /** * Returns the route. */ - override def knoraApiPath: Route = createResource ~ updateResourceMetadata ~ getResourcesInProject ~ - getResourceHistory ~ getResources ~ getResourcesPreview ~ getResourcesTei ~ - getResourcesGraph ~ deleteResource ~ eraseResource - - private def createResource: Route = path(ResourcesBasePath) { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + createResource(featureFactoryConfig) ~ + updateResourceMetadata(featureFactoryConfig) ~ + getResourcesInProject(featureFactoryConfig) ~ + getResourceHistory(featureFactoryConfig) ~ + getResources(featureFactoryConfig) ~ + getResourcesPreview(featureFactoryConfig) ~ + getResourcesTei(featureFactoryConfig) ~ + getResourcesGraph(featureFactoryConfig) ~ + deleteResource(featureFactoryConfig) ~ + eraseResource(featureFactoryConfig) + + private def createResource(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath) { post { entity(as[String]) { jsonRequest => requestContext => { @@ -86,6 +95,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -97,7 +107,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def updateResourceMetadata: Route = path(ResourcesBasePath) { + private def updateResourceMetadata(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath) { put { entity(as[String]) { jsonRequest => requestContext => { @@ -119,6 +129,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -130,7 +141,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResourcesInProject: Route = path(ResourcesBasePath) { + private def getResourcesInProject(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath) { get { requestContext => { val projectIri: SmartIri = RouteUtilV2.getProject(requestContext).getOrElse(throw BadRequestException(s"This route requires the request header ${RouteUtilV2.PROJECT_HEADER}")) @@ -175,11 +186,12 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) ) RouteUtilV2.runRdfRouteWithFuture( - requestMessageFuture, - requestContext, - settings, - responderManager, - log, + requestMessageF = requestMessageFuture, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log, targetSchema = ApiV2Complex, schemaOptions = schemaOptions ) @@ -187,7 +199,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResourceHistory: Route = path(ResourcesBasePath / "history" / Segment) { resourceIriStr: IRI => + private def getResourceHistory(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath / "history" / Segment) { resourceIriStr: IRI => get { requestContext => { val resourceIri = stringFormatter.validateAndEscapeIri(resourceIriStr, throw BadRequestException(s"Invalid resource IRI: $resourceIriStr")) @@ -207,6 +219,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -217,7 +230,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResources: Route = path(ResourcesBasePath / Segments) { resIris: Seq[String] => + private def getResources(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath / Segments) { resIris: Seq[String] => get { requestContext => { @@ -260,6 +273,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -270,7 +284,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResourcesPreview: Route = path("v2" / "resourcespreview" / Segments) { resIris: Seq[String] => + private def getResourcesPreview(featureFactoryConfig: FeatureFactoryConfig): 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}") @@ -289,6 +303,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -299,7 +314,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResourcesTei: Route = path("v2" / "tei" / Segment) { resIri: String => + private def getResourcesTei(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "tei" / Segment) { resIri: String => get { requestContext => { @@ -330,6 +345,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runTEIXMLRoute( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -339,7 +355,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResourcesGraph: Route = path("v2" / "graph" / Segment) { resIriStr: String => + private def getResourcesGraph(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "graph" / Segment) { resIriStr: String => get { requestContext => { val resourceIri: IRI = stringFormatter.validateAndEscapeIri(resIriStr, throw BadRequestException(s"Invalid resource IRI: <$resIriStr>")) @@ -378,6 +394,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -388,7 +405,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def deleteResource: Route = path(ResourcesBasePath / "delete") { + private def deleteResource(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath / "delete") { post { entity(as[String]) { jsonRequest => requestContext => { @@ -410,6 +427,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -421,7 +439,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def eraseResource: Route = path(ResourcesBasePath / "erase") { + private def eraseResource(featureFactoryConfig: FeatureFactoryConfig): Route = path(ResourcesBasePath / "erase") { post { entity(as[String]) { jsonRequest => requestContext => { @@ -443,6 +461,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -453,6 +472,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } } + /** * Gets the Iri of the property that represents the text of the resource. * 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 2d7ce143c8..b5945bf2b1 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 @@ -23,6 +23,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser import org.knora.webapi.messages.v2.responder.searchmessages._ @@ -44,8 +45,15 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * Returns the route. */ - override def knoraApiPath: Route = fullTextSearchCount ~ fullTextSearch ~ gravsearchCountGet ~ gravsearchCountPost ~ - gravsearchGet ~ gravsearchPost ~ searchByLabelCount ~ searchByLabel + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + fullTextSearchCount(featureFactoryConfig) ~ + fullTextSearch(featureFactoryConfig) ~ + gravsearchCountGet(featureFactoryConfig) ~ + gravsearchCountPost(featureFactoryConfig) ~ + gravsearchGet(featureFactoryConfig) ~ + gravsearchPost(featureFactoryConfig) ~ + searchByLabelCount(featureFactoryConfig) ~ + searchByLabel(featureFactoryConfig) /** * Gets the requested offset. Returns zero if no offset is indicated. @@ -144,7 +152,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def fullTextSearchCount: Route = path("v2" / "search" / "count" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space + private def fullTextSearchCount(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "search" / "count" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => val searchString = stringFormatter.toSparqlEncodedString(searchval, throw BadRequestException(s"Invalid search string: '$searchval'")) @@ -174,6 +182,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -183,7 +192,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def fullTextSearch: Route = path("v2" / "search" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space + private def fullTextSearch(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "search" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => { val searchString = stringFormatter.toSparqlEncodedString(searchval, throw BadRequestException(s"Invalid search string: '$searchval'")) @@ -221,6 +230,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -231,7 +241,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def gravsearchCountGet: Route = path("v2" / "searchextended" / "count" / Segment) { gravsearchQuery => // Segment is a URL encoded string representing a Gravsearch query + private def gravsearchCountGet(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "searchextended" / "count" / Segment) { gravsearchQuery => // Segment is a URL encoded string representing a Gravsearch query get { requestContext => { val constructQuery = GravsearchParser.parseQuery(gravsearchQuery) @@ -243,6 +253,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -253,7 +264,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def gravsearchCountPost: Route = path("v2" / "searchextended" / "count") { + private def gravsearchCountPost(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "searchextended" / "count") { post { entity(as[String]) { gravsearchQuery => requestContext => { @@ -265,6 +276,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -276,7 +288,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def gravsearchGet: Route = path("v2" / "searchextended" / Segment) { sparql => // Segment is a URL encoded string representing a Gravsearch query + private def gravsearchGet(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "searchextended" / Segment) { sparql => // Segment is a URL encoded string representing a Gravsearch query get { requestContext => { val constructQuery = GravsearchParser.parseQuery(sparql) @@ -295,6 +307,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -305,7 +318,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def gravsearchPost: Route = path("v2" / "searchextended") { + private def gravsearchPost(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "searchextended") { post { entity(as[String]) { gravsearchQuery => requestContext => { @@ -325,6 +338,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -336,7 +350,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def searchByLabelCount: Route = path("v2" / "searchbylabel" / "count" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space + private def searchByLabelCount(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "searchbylabel" / "count" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => { @@ -364,6 +378,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -374,7 +389,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def searchByLabel: Route = path("v2" / "searchbylabel" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space + private def searchByLabel(featureFactoryConfig: FeatureFactoryConfig): Route = path("v2" / "searchbylabel" / Segment) { searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => { val searchString = stringFormatter.toSparqlEncodedString(searchval, throw BadRequestException(s"Invalid search string: '$searchval'")) @@ -407,6 +422,7 @@ class SearchRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessage, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, 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 a1e288450d..ec9e0b9388 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 @@ -27,6 +27,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.JsonLDUtil @@ -44,7 +45,7 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w /** * Returns the route. */ - override def knoraApiPath: Route = { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { path("v2" / "standoff" / Segment / Segment / Segment) { (resourceIriStr: String, valueIriStr: String, offsetStr: String) => get { @@ -79,6 +80,7 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -149,6 +151,7 @@ class StandoffRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) w RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, 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 8bbe49314e..3f4575317f 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 @@ -26,6 +26,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.util.{JsonLDDocument, JsonLDUtil} @@ -49,9 +50,13 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * Returns the route. */ - override def knoraApiPath: Route = getValue ~ createValue ~ updateValue ~ deleteValue + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = + getValue(featureFactoryConfig) ~ + createValue(featureFactoryConfig) ~ + updateValue(featureFactoryConfig) ~ + deleteValue(featureFactoryConfig) - private def getValue: Route = path(ValuesBasePath / Segment / Segment) { (resourceIriStr: IRI, valueUuidStr: String) => + private def getValue(featureFactoryConfig: FeatureFactoryConfig): Route = path(ValuesBasePath / Segment / Segment) { (resourceIriStr: IRI, valueUuidStr: String) => get { requestContext => { val resourceIri: SmartIri = resourceIriStr.toSmartIriWithErr(throw BadRequestException(s"Invalid resource IRI: $resourceIriStr")) @@ -94,6 +99,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -104,7 +110,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def createValue: Route = path(ValuesBasePath) { + private def createValue(featureFactoryConfig: FeatureFactoryConfig): Route = path(ValuesBasePath) { post { entity(as[String]) { jsonRequest => requestContext => { @@ -126,6 +132,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -137,7 +144,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def updateValue: Route = path(ValuesBasePath) { + private def updateValue(featureFactoryConfig: FeatureFactoryConfig): Route = path(ValuesBasePath) { put { entity(as[String]) { jsonRequest => requestContext => { @@ -159,6 +166,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, @@ -170,7 +178,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def deleteValue: Route = path(ValuesBasePath / "delete") { + private def deleteValue(featureFactoryConfig: FeatureFactoryConfig): Route = path(ValuesBasePath / "delete") { post { entity(as[String]) { jsonRequest => requestContext => { @@ -192,6 +200,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit RouteUtilV2.runRdfRouteWithFuture( requestMessageF = requestMessageFuture, requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, settings = settings, responderManager = responderManager, log = log, 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 1c0d74ebac..7d205bedb2 100644 --- a/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala +++ b/webapi/src/main/scala/org/knora/webapi/settings/KnoraSettings.scala @@ -21,20 +21,25 @@ package org.knora.webapi.settings import java.io.File import java.nio.file.{Files, Paths} +import java.time.Instant import akka.ConfigurationException import akka.actor.{ActorSystem, ExtendedActorSystem, Extension, ExtensionId, ExtensionIdProvider} -import com.typesafe.config.{Config, ConfigValue} -import org.knora.webapi.exceptions.FileWriteException +import akka.event.LoggingAdapter +import com.typesafe.config.{Config, ConfigObject, ConfigValue} +import org.knora.webapi.exceptions.{FeatureToggleException, FileWriteException} import org.knora.webapi.util.cache.CacheUtil.KnoraCacheConfig import scala.collection.JavaConverters._ import scala.concurrent.duration._ +import scala.util.{Failure, Success, Try} /** * Reads application settings that come from `application.conf`. */ -class KnoraSettingsImpl(config: Config) extends Extension { +class KnoraSettingsImpl(config: Config, log: LoggingAdapter) extends Extension { + + import KnoraSettings._ // print config val printExtendedConfig: Boolean = config.getBoolean("app.print-extended-config") @@ -245,6 +250,73 @@ class KnoraSettingsImpl(config: Config) extends Extension { None } + val featureToggles: Set[FeatureToggleBaseConfig] = if (config.hasPath(featureTogglesPath)) { + Try { + config.getObject(featureTogglesPath).asScala.toMap.map { + case (featureName: String, featureConfigValue: ConfigValue) => + val featureConfig: Config = featureConfigValue match { + case configObject: ConfigObject => configObject.toConfig + case _ => throw FeatureToggleException(s"The feature toggle configuration $featureName must be an object") + } + + val description: String = featureConfig.getString(descriptionKey) + val availableVersions: Seq[Int] = featureConfig.getIntList(availableVersionsKey).asScala.map(_.intValue).toVector + + if (availableVersions.isEmpty) { + throw FeatureToggleException(s"Feature toggle $featureName has no version numbers") + } + + for ((version: Int, index: Int) <- availableVersions.zipWithIndex) { + if (version != index + 1) { + throw FeatureToggleException(s"The version numbers of feature toggle $featureName must be an ascending sequence of consecutive integers starting from 1") + } + } + + val defaultVersion: Int = featureConfig.getInt(defaultVersionKey) + + if (!availableVersions.contains(defaultVersion)) { + throw FeatureToggleException(s"Invalid default version number $defaultVersion for feature toggle $featureName") + } + + val enabledByDefault: Boolean = featureConfig.getBoolean(enabledByDefaultKey) + val overrideAllowed: Boolean = featureConfig.getBoolean(overrideAllowedKey) + + val expirationDate: Option[Instant] = if (featureConfig.hasPath(expirationDateKey)) { + val definedExpirationDate: Instant = Instant.parse(featureConfig.getString(expirationDateKey)) + + if (Instant.ofEpochMilli(System.currentTimeMillis).isAfter(definedExpirationDate)) { + log.warning(s"Feature toggle $featureName has expired") + } + + Some(definedExpirationDate) + } else { + None + } + + val developerEmails: Set[String] = featureConfig.getStringList(developerEmailsKey).asScala.toSet + + FeatureToggleBaseConfig( + featureName = featureName, + description = description, + availableVersions = availableVersions, + defaultVersion = defaultVersion, + enabledByDefault = enabledByDefault, + overrideAllowed = overrideAllowed, + expirationDate = expirationDate, + developerEmails = developerEmails + ) + }.toSet + } match { + case Success(toggles) => toggles + case Failure(ex) => + ex match { + case fte: FeatureToggleException => throw fte + case other => throw FeatureToggleException(s"Invalid feature toggle configuration: ${other.getMessage}", Some(ex)) + } + } + } else { + Set.empty + } } object KnoraSettings extends ExtensionId[KnoraSettingsImpl] with ExtensionIdProvider { @@ -252,10 +324,43 @@ object KnoraSettings extends ExtensionId[KnoraSettingsImpl] with ExtensionIdProv override def lookup(): KnoraSettings.type = KnoraSettings override def createExtension(system: ExtendedActorSystem) = - new KnoraSettingsImpl(system.settings.config) + new KnoraSettingsImpl(system.settings.config, akka.event.Logging(system, this.getClass)) /** * Java API: retrieve the Settings extension for the given system. */ override def get(system: ActorSystem): KnoraSettingsImpl = super.get(system) -} \ No newline at end of file + + val featureTogglesPath: String = "app.feature-toggles" + val descriptionKey: String = "description" + val availableVersionsKey: String = "available-versions" + val developerEmailsKey: String = "developer-emails" + val expirationDateKey: String = "expiration-date" + val enabledByDefaultKey: String = "enabled-by-default" + val defaultVersionKey: String = "default-version" + val overrideAllowedKey: String = "override-allowed" + + /** + * Represents the base configuration of a feature toggle. + * + * @param featureName the name of the feature. + * @param description a description of the feature. + * @param availableVersions the available versions of the feature. + * @param defaultVersion the version of the feature that should be enabled by default. + * @param enabledByDefault `true` if the feature should be enabled by default, `false` if it should be + * disabled by default. + * @param overrideAllowed `true` if this configuration can be overridden, e.g. by per-request feature + * toggle configuration. + * @param expirationDate the expiration date of the feature. + * @param developerEmails one or more email addresses of developers who can be contacted about the feature. + */ + case class FeatureToggleBaseConfig(featureName: String, + description: String, + availableVersions: Seq[Int], + defaultVersion: Int, + enabledByDefault: Boolean, + overrideAllowed: Boolean, + expirationDate: Option[Instant], + developerEmails: Set[String]) + +} diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/TriplestoreManager.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/TriplestoreManager.scala index c70d8144a3..78e2457323 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/TriplestoreManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/TriplestoreManager.scala @@ -23,7 +23,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.event.LoggingReceive import akka.routing.FromConfig import org.knora.webapi.core.ActorMaker -import org.knora.webapi.exceptions.UnsuportedTriplestoreException +import org.knora.webapi.exceptions.UnsupportedTriplestoreException import org.knora.webapi.messages.store.triplestoremessages.UpdateRepositoryRequest import org.knora.webapi.messages.util.FakeTriplestore import org.knora.webapi.settings.{KnoraDispatchers, KnoraSettings, TriplestoreTypes, _} @@ -78,7 +78,7 @@ class TriplestoreManager(appActor: ActorRef) extends Actor with ActorLogging { storeActorRef = settings.triplestoreType match { case TriplestoreTypes.HttpGraphDBSE | TriplestoreTypes.HttpGraphDBFree | TriplestoreTypes.HttpFuseki => makeActor(FromConfig.props(Props[HttpTriplestoreConnector]).withDispatcher(KnoraDispatchers.KnoraActorDispatcher), name = HttpTriplestoreActorName) case TriplestoreTypes.EmbeddedJenaTdb => makeActor(Props[JenaTDBActor], name = EmbeddedJenaActorName) - case unknownType => throw UnsuportedTriplestoreException(s"Embedded triplestore type $unknownType not supported") + case unknownType => throw UnsupportedTriplestoreException(s"Embedded triplestore type $unknownType not supported") } log.debug("TriplestoreManagerActor: finished with preStart") diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala index d412a2aacc..fda8b113e3 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/http/HttpTriplestoreConnector.scala @@ -132,7 +132,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { s"/${settings.triplestoreDatabaseName}/query" } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } private val sparqlUpdatePath: String = if (triplestoreType == TriplestoreTypes.HttpGraphDBSE | triplestoreType == TriplestoreTypes.HttpGraphDBFree) { @@ -140,7 +140,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { s"/${settings.triplestoreDatabaseName}/update" } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } private val checkRepositoryPath: String = if (triplestoreType == TriplestoreTypes.HttpGraphDBSE | triplestoreType == TriplestoreTypes.HttpGraphDBFree) { @@ -148,7 +148,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { "/$/server" } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } private val graphPath: String = if (triplestoreType == TriplestoreTypes.HttpGraphDBSE | triplestoreType == TriplestoreTypes.HttpGraphDBFree) { @@ -156,7 +156,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { s"/${settings.triplestoreDatabaseName}/get" } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } private val repositoryDownloadPath = if (triplestoreType == TriplestoreTypes.HttpGraphDBSE | triplestoreType == TriplestoreTypes.HttpGraphDBFree) { @@ -164,7 +164,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { s"/${settings.triplestoreDatabaseName}" } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } private val repositoryUploadPath = repositoryDownloadPath @@ -498,8 +498,6 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat response.recover { case t: Exception => throw TriplestoreResponseException("Reset: Failed to execute DROP ALL", t, log) } - - response } /** @@ -586,7 +584,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { checkFusekiTriplestore() } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } } @@ -758,7 +756,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { uriBuilder.setParameter("graph", s"$graphIri") } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } uriBuilder.build() @@ -864,7 +862,7 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat } else if (triplestoreType == TriplestoreTypes.HttpFuseki) { // do nothing } else { - throw UnsuportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") + throw UnsupportedTriplestoreException(s"Unsupported triplestore type: $triplestoreType") } val httpGet = new HttpGet(uriBuilder.build()) @@ -1003,8 +1001,6 @@ class HttpTriplestoreConnector extends Actor with ActorLogging with Instrumentat log.error(e, s"Failed to connect to triplestore") throw TriplestoreConnectionException(s"Failed to connect to triplestore", e, log) } - - triplestoreResponseTry } def returnResponseAsString(response: CloseableHttpResponse): String = { diff --git a/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala index 8955a0bdc8..66ba02da40 100644 --- a/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/E2ESpec.scala @@ -19,11 +19,10 @@ package org.knora.webapi -import java.io.{ByteArrayInputStream, File, StringReader} -import java.nio.file.{Files, Path} -import java.util.zip.{ZipEntry, ZipFile, ZipInputStream} +import java.io.{File, StringReader} import akka.actor.{ActorRef, ActorSystem, Props} +import akka.event.LoggingAdapter import akka.http.scaladsl.Http import akka.http.scaladsl.client.RequestBuilding import akka.http.scaladsl.model._ @@ -43,10 +42,8 @@ import org.knora.webapi.util.{FileUtil, StartupUtils} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.{BeforeAndAfterAll, Suite} -import resource.managed import spray.json._ -import scala.collection.JavaConverters._ import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} import scala.languageFeature.postfixOps @@ -83,7 +80,7 @@ class E2ESpec(_system: ActorSystem) extends Core with StartupUtils with Triplest /* Needs to be initialized before any responders */ StringFormatter.initForTest() - val log = akka.event.Logging(system, this.getClass) + val log: LoggingAdapter = akka.event.Logging(system, this.getClass) lazy val appActor: ActorRef = system.actorOf(Props(new ApplicationActor with LiveManagers), name = APPLICATION_MANAGER_ACTOR_NAME) diff --git a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala index 56ebcaaa90..aec0a11118 100644 --- a/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/R2RSpec.scala @@ -57,8 +57,11 @@ import scala.language.postfixOps class R2RSpec extends Core with StartupUtils with Suite with ScalatestRouteTest with AnyWordSpecLike with Matchers with BeforeAndAfterAll with LazyLogging { /* needed by the core trait */ - implicit lazy val _system: ActorSystem = ActorSystem(actorSystemNameFrom(getClass), TestContainers.PortConfig.withFallback(ConfigFactory.load())) + implicit lazy val _system: ActorSystem = ActorSystem(actorSystemNameFrom(getClass), + TestContainers.PortConfig.withFallback(ConfigFactory.parseString(testConfigSource).withFallback(ConfigFactory.load()))) + implicit lazy val settings: KnoraSettingsImpl = KnoraSettings(_system) + lazy val executionContext: ExecutionContext = _system.dispatchers.lookup(KnoraDispatchers.KnoraActorDispatcher) // override so that we can use our own system diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/e2e/BUILD.bazel index 8644272f60..cacb8638ad 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/e2e/BUILD.bazel @@ -102,3 +102,21 @@ scala_test( "//webapi:test_library", ] + BASE_TEST_DEPENDENCIES_WITH_JSON, ) + +scala_test( + name = "FeatureToggleR2RSpec", + size = "small", # 60s + srcs = [ + "FeatureToggleR2RSpec.scala", + ], + data = [ + "//knora-ontologies", + "//test_data", + ], + jvm_flags = ["-Dconfig.resource=fuseki.conf"], + # unused_dependency_checker_mode = "warn", + deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ + "//webapi:main_library", + "//webapi:test_library", + ] + BASE_TEST_DEPENDENCIES_WITH_JSON, +) diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/FeatureToggleR2RSpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/FeatureToggleR2RSpec.scala new file mode 100644 index 0000000000..ef8a7ce567 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/e2e/FeatureToggleR2RSpec.scala @@ -0,0 +1,341 @@ +/* + * Copyright © 2015-2019 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi.e2e + +import akka.actor.ActorSystem +import akka.http.scaladsl.model.headers.RawHeader +import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCodes} +import akka.http.scaladsl.server.Directives.{get, path} +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.testkit.RouteTestTimeout +import akka.http.scaladsl.util.FastFuture +import com.typesafe.config.ConfigFactory +import org.knora.webapi.{R2RSpec, TestContainers} +import org.knora.webapi.feature._ +import org.knora.webapi.routing.{KnoraRoute, KnoraRouteData, KnoraRouteFactory} + +import scala.concurrent.ExecutionContextExecutor + +/** + * Tests feature toggles that replace implementations of API routes. + */ +class FeatureToggleR2RSpec extends R2RSpec { + // Don't take feature toggles from application.conf, just take them from the config in this spec. + override implicit lazy val _system: ActorSystem = ActorSystem(actorSystemNameFrom(getClass), + TestContainers.PortConfig.withFallback(ConfigFactory.parseString(testConfigSource).withFallback(ConfigFactory.load().withoutPath("app.feature-toggles")))) + + // Some feature toggles for testing. + override def testConfigSource: String = + """app { + | feature-toggles { + | new-foo { + | description = "Replace the old foo routes with new ones." + | + | available-versions = [ 1, 2 ] + | default-version = 1 + | enabled-by-default = yes + | override-allowed = yes + | + | developer-emails = [ + | "Benjamin Geer " + | ] + | } + | + | new-bar { + | description = "Replace the old bar routes with new ones." + | + | available-versions = [ 1 ] + | default-version = 1 + | enabled-by-default = yes + | override-allowed = yes + | + | developer-emails = [ + | "Benjamin Geer " + | ] + | } + | + | new-baz { + | description = "Replace the old baz routes with new ones." + | + | available-versions = [ 1 ] + | default-version = 1 + | enabled-by-default = no + | override-allowed = no + | + | developer-emails = [ + | "Benjamin Geer " + | ] + | } + | } + |} + """.stripMargin + + /** + * A test implementation of a route feature that handles HTTP GET requests. + * + * @param pathStr the route path. + * @param featureName the name of the feature. + * @param routeData a [[KnoraRouteData]] providing access to the application. + */ + class TestRouteFeature(pathStr: String, featureName: String, routeData: KnoraRouteData) extends KnoraRoute(routeData) with Feature { + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = path(pathStr) { + get { + requestContext => + // Return an HTTP response that says which feature implementation is being used. + val httpResponse = FastFuture.successful { + featureFactoryConfig.addHeaderToHttpResponse( + HttpResponse( + status = StatusCodes.OK, + entity = HttpEntity( + contentType = ContentTypes.`application/json`, + string = s"You are using $featureName" + ) + ) + ) + } + + requestContext.complete(httpResponse) + } + } + } + + /** + * A feature factory that constructs implementations of [[FooRoute]]. + */ + class FooRouteFeatureFactory(routeData: KnoraRouteData) extends KnoraRouteFactory(routeData) + with FeatureFactory { + + // A trait for version numbers of the new 'foo' feature. + sealed trait NewFooVersion extends Version + + // Represents version 1 of the new 'foo' feature. + case object NEW_FOO_1 extends NewFooVersion + + // Represents version 2 of the new 'foo' feature. + case object NEW_FOO_2 extends NewFooVersion + + // The old 'foo' feature implementation. + private val oldFoo = new TestRouteFeature(pathStr = "foo", featureName = "the old foo", routeData = routeData) + + // The new 'foo' feature implementation, version 1. + private val newFoo1 = new TestRouteFeature(pathStr = "foo", featureName = "the new foo, version 1", routeData = routeData) + + // The new 'foo' feature implementation, version 2. + private val newFoo2 = new TestRouteFeature(pathStr = "foo", featureName = "the new foo, version 2", routeData = routeData) + + /** + * Constructs an implementation of the 'foo' route according to the feature factory + * configuration. + * + * @param featureFactoryConfig the per-request feature factory configuration. + * @return a route configured with the features enabled by the feature factory configuration. + */ + def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + // Get the 'new-foo' feature toggle. + val fooToggle: FeatureToggle = featureFactoryConfig.getToggle("new-foo") + + // Choose a route according to the toggle state. + val route: KnoraRoute = fooToggle.getMatchableState(NEW_FOO_1, NEW_FOO_2) match { + case Off => oldFoo + case On(NEW_FOO_1) => newFoo1 + case On(NEW_FOO_2) => newFoo2 + } + + // Ask the route implementation for its routing function, and return that function. + route.makeRoute(featureFactoryConfig) + } + } + + /** + * A feature factory that constructs implementations of [[BarRoute]]. + */ + class BarRouteFeatureFactory(routeData: KnoraRouteData) extends KnoraRouteFactory(routeData) + with FeatureFactory { + + // The old 'bar' feature implementation. + private val oldBar = new TestRouteFeature(pathStr = "bar", featureName = "the old bar", routeData = routeData) + + // The new 'bar' feature implementation. + private val newBar = new TestRouteFeature(pathStr = "bar", featureName = "the new bar", routeData = routeData) + + def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + // Is the 'new-bar' feature toggle enabled? + val route: KnoraRoute = if (featureFactoryConfig.getToggle("new-bar").isEnabled) { + // Yes. Use the new implementation. + newBar + } else { + // No. Use the old implementation. + oldBar + } + + // Ask the route implementation for its routing function, and return that function. + route.makeRoute(featureFactoryConfig) + } + } + + /** + * A feature factory that constructs implementations of [[BazRoute]]. + */ + class BazRouteFeatureFactory(routeData: KnoraRouteData) extends KnoraRouteFactory(routeData) + with FeatureFactory { + + // The old 'baz' feature implementation. + private val oldBaz = new TestRouteFeature(pathStr = "baz", featureName = "the old baz", routeData = routeData) + + // The new 'baz' feature implementation. + private val newBaz = new TestRouteFeature(pathStr = "baz", featureName = "the new baz", routeData = routeData) + + def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + // Is the 'new-baz' feature toggle enabled? + val route: KnoraRoute = if (featureFactoryConfig.getToggle("new-baz").isEnabled) { + // Yes. Use the new implementation. + newBaz + } else { + // No. Use the old implementation. + oldBaz + } + + route.makeRoute(featureFactoryConfig) + } + } + + /** + * A façade route that uses implementations constructed by [[FooRouteFeatureFactory]]. + */ + class FooRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) { + private val featureFactory = new FooRouteFeatureFactory(routeData) + + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + featureFactory.makeRoute(featureFactoryConfig) + } + } + + /** + * A façade route that uses implementations constructed by [[BarRouteFeatureFactory]]. + */ + class BarRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) { + private val featureFactory = new BarRouteFeatureFactory(routeData) + + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + featureFactory.makeRoute(featureFactoryConfig) + } + } + + /** + * A façade route that uses implementations constructed by [[BazRouteFeatureFactory]]. + */ + class BazRoute(routeData: KnoraRouteData) extends KnoraRoute(routeData) { + private val featureFactory = new BazRouteFeatureFactory(routeData) + + override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = { + featureFactory.makeRoute(featureFactoryConfig) + } + } + + // The façade route instances that we are going to test. + private val fooRoute = new FooRoute(routeData).knoraApiPath + private val bazRoute = new BazRoute(routeData).knoraApiPath + + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) + + implicit val ec: ExecutionContextExecutor = system.dispatcher + + /** + * Parses the HTTP response header that lists the configured feature toggles. + * + * @param response the HTTP response. + * @return a string per toggle. + */ + private def parseResponseHeader(response: HttpResponse): Set[String] = { + response.headers.find(_.lowercaseName == FeatureToggle.RESPONSE_HEADER_LOWERCASE) match { + case Some(header) => header.value.split(',').toSet + case None => Set.empty + } + } + + "The feature toggle framework" should { + "use default toggles" in { + Get(s"/foo") ~> fooRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.OK, responseStr) + assert(responseStr == "You are using the new foo, version 1") + assert(parseResponseHeader(response) == Set("new-foo:1=on", "new-bar:1=on", "new-baz=off")) + } + } + + "turn off a toggle" in { + Get(s"/foo").addHeader(RawHeader(FeatureToggle.REQUEST_HEADER, "new-foo=off")) ~> fooRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.OK, responseStr) + assert(responseStr == "You are using the old foo") + assert(parseResponseHeader(response) == Set("new-foo=off", "new-bar:1=on", "new-baz=off")) + } + } + + "override the default toggle version" in { + Get(s"/foo").addHeader(RawHeader(FeatureToggle.REQUEST_HEADER, "new-foo:2=on")) ~> fooRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.OK, responseStr) + assert(responseStr == "You are using the new foo, version 2") + assert(parseResponseHeader(response) == Set("new-foo:2=on", "new-bar:1=on", "new-baz=off")) + } + } + + "not enable a toggle without specifying the version number" in { + Get(s"/foo").addHeader(RawHeader(FeatureToggle.REQUEST_HEADER, "new-foo=on")) ~> fooRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.BadRequest, responseStr) + assert(responseStr.contains("You must specify a version number to enable feature toggle new-foo")) + } + } + + "not enable a nonexistent version of a toggle" in { + Get(s"/foo").addHeader(RawHeader(FeatureToggle.REQUEST_HEADER, "new-foo:3=on")) ~> fooRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.BadRequest, responseStr) + assert(responseStr.contains("Feature toggle new-foo has no version 3")) + } + } + + "not accept a version number when disabling a toggle" in { + Get(s"/foo").addHeader(RawHeader(FeatureToggle.REQUEST_HEADER, "new-foo:2=off")) ~> fooRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.BadRequest, responseStr) + assert(responseStr.contains("You cannot specify a version number when disabling feature toggle new-foo")) + } + } + + "not override a default toggle if the base configuration doesn't allow it" in { + Get(s"/baz").addHeader(RawHeader(FeatureToggle.REQUEST_HEADER, "new-baz=on")) ~> bazRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.BadRequest, responseStr) + assert(responseStr.contains("Feature toggle new-baz cannot be overridden")) + } + } + + "not accept two settings for the same toggle" in { + Get(s"/baz").addHeader(RawHeader(FeatureToggle.REQUEST_HEADER, "new-foo=off,new-foo:2=on")) ~> bazRoute ~> check { + val responseStr = responseAs[String] + assert(status == StatusCodes.BadRequest, responseStr) + assert(responseStr.contains("You cannot set the same feature toggle more than once per request")) + } + } + } +} 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 400bf0a5fb..094ef4310d 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 @@ -22,13 +22,13 @@ package org.knora.webapi.e2e.http import akka.actor.ActorSystem import akka.http.scaladsl.model._ import akka.http.scaladsl.testkit.RouteTestTimeout -import com.typesafe.config.ConfigFactory +import com.typesafe.config.{Config, ConfigFactory} import org.knora.webapi.E2ESpec import org.knora.webapi.http.version.ServerVersion object ServerVersionE2ESpec { - val config = ConfigFactory.parseString( + val config: Config = ConfigFactory.parseString( """ akka.loglevel = "DEBUG" akka.stdout-loglevel = "DEBUG" @@ -40,14 +40,14 @@ object ServerVersionE2ESpec { */ class ServerVersionE2ESpec extends E2ESpec(ServerVersionE2ESpec.config) { - implicit def default(implicit system: ActorSystem) = RouteTestTimeout(settings.defaultTimeout) + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(settings.defaultTimeout) "The Server" should { "return the custom 'Server' header with every response" in { val request = Get(baseApiUrl + s"/admin/projects") val response: HttpResponse = singleAwaitingRequest(request) - response.headers should contain (ServerVersion.serverVersionHeader()) + response.headers should contain (ServerVersion.serverVersionHeader) response.headers.find(_.name == "Server") match { case Some(serverHeader: HttpHeader) => serverHeader.value() should include ("webapi/") diff --git a/webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala b/webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala index b097750148..fe0dc468dc 100644 --- a/webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala @@ -31,7 +31,7 @@ class ServerVersionSpec extends AnyWordSpecLike with Matchers { "The server version header" should { "contain the necessary information" in { - val header: Server = ServerVersion.serverVersionHeader() + val header: Server = ServerVersion.serverVersionHeader header.toString() should include("webapi/") header.toString() should include("akka-http/") }