From ed1cbd05cbc2753655a16b21d57bc64cfde74f03 Mon Sep 17 00:00:00 2001 From: irinaschubert Date: Fri, 26 Aug 2022 09:05:45 +0200 Subject: [PATCH] chore(feature-toggles): remove remnants of feature toggles (DEV-217) (#2176) * remove remnants of feature toggles * redo changes in PR template * refactor admin list routes * remove unused imports * fix formatting * refactor unnecessary companion objects in routes * fix codacy issues * fix codacy issues * Update expected-client-test-data.txt * add tests removed by accident Co-authored-by: Ivan Subotic <400790+subotic@users.noreply.github.com> --- docs/03-apis/api-v2/query-language.md | 2 - docs/03-apis/feature-toggles.md | 64 -- docs/03-apis/index.md | 2 - .../design/principles/feature-toggles.md | 263 --------- docs/05-internals/design/principles/index.md | 1 - .../05-internals/design/principles/rdf-api.md | 27 +- .../src/main/scala/dsp/errors/Errors.scala | 8 - mkdocs.yml | 2 - webapi/scripts/expected-client-test-data.txt | 2 - .../messages/util/rdf/RdfFeatureFactory.scala | 4 - .../org/knora/webapi/routing/KnoraRoute.scala | 2 - .../webapi/routing/SwaggerApiDocsRoute.scala | 4 +- .../webapi/routing/admin/GroupsRouteADM.scala | 20 +- .../webapi/routing/admin/ListsRouteADM.scala | 6 +- .../routing/admin/ProjectsRouteADM.scala | 44 +- .../webapi/routing/admin/UsersRouteADM.scala | 44 +- .../admin/lists/CreateListItemsRouteADM.scala | 196 +++++++ .../admin/lists/DeleteListItemsRouteADM.scala | 14 +- .../admin/lists/GetListItemsRouteADM.scala | 168 ++++++ .../admin/lists/OldListsRouteADMFeature.scala | 404 ------------- .../admin/lists/UpdateListItemsRouteADM.scala | 103 +++- .../CreatePermissionRouteADM.scala | 10 +- .../DeletePermissionRouteADM.scala | 8 +- .../permissions/GetPermissionsRouteADM.scala | 14 +- .../UpdatePermissionRouteADM.scala | 14 +- .../webapi/routing/v2/OntologiesRouteV2.scala | 60 +- .../webapi/routing/v2/ResourcesRouteV2.scala | 26 +- .../webapi/routing/v2/ValuesRouteV2.scala | 14 +- webapi/src/test/resources/logback-test.xml | 2 +- ...a => CreateListItemsRouteADME2ESpec.scala} | 546 +----------------- .../DeleteListItemsRouteADME2ESpec.scala | 34 +- .../lists/GetListItemsRouteADME2ESpec.scala | 271 +++++++++ .../UpdateListItemsRouteADME2ESpec.scala | 251 +++++++- 33 files changed, 1118 insertions(+), 1512 deletions(-) delete mode 100644 docs/03-apis/feature-toggles.md delete mode 100644 docs/05-internals/design/principles/feature-toggles.md create mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala rename webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/{OldListsRouteADMFeatureE2ESpec.scala => CreateListItemsRouteADME2ESpec.scala} (52%) create mode 100644 webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala diff --git a/docs/03-apis/api-v2/query-language.md b/docs/03-apis/api-v2/query-language.md index 48f36c0d9b..420a980164 100644 --- a/docs/03-apis/api-v2/query-language.md +++ b/docs/03-apis/api-v2/query-language.md @@ -1201,8 +1201,6 @@ The query performance of triplestores, such as Fuseki, is highly dependent on th patterns. To improve performance, Gravsearch automatically reorders the statement patterns in the WHERE clause according to their dependencies on each other, to minimise the number of possible matches for each pattern. -This optimization can be controlled using `gravsearch-dependency-optimisation` -[feature toggle](../feature-toggles.md), which is turned on by default. Consider the following Gravsearch query: diff --git a/docs/03-apis/feature-toggles.md b/docs/03-apis/feature-toggles.md deleted file mode 100644 index e240edef86..0000000000 --- a/docs/03-apis/feature-toggles.md +++ /dev/null @@ -1,64 +0,0 @@ - - -# 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 - -DSP-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 7a405cd408..ef282e2ea1 100644 --- a/docs/03-apis/index.md +++ b/docs/03-apis/index.md @@ -15,5 +15,3 @@ The DSP APIs include: administering projects that use Knora as well as Knora itself. * The DSP [Util API](api-util/index.md), which is intended to be used for information retrieval about the DSP-stack itself. - -DSP API v2 and the admin API support [Feature Toggles](feature-toggles.md). diff --git a/docs/05-internals/design/principles/feature-toggles.md b/docs/05-internals/design/principles/feature-toggles.md deleted file mode 100644 index b04526672f..0000000000 --- a/docs/05-internals/design/principles/feature-toggles.md +++ /dev/null @@ -1,263 +0,0 @@ - - -# 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 - -DSP-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 d9619f868e..6c4c734d01 100644 --- a/docs/05-internals/design/principles/index.md +++ b/docs/05-internals/design/principles/index.md @@ -13,4 +13,3 @@ - [Triplestore Updates](triplestore-updates.md) - [Consistency Checking](consistency-checking.md) - [Authentication](authentication.md) -- [Feature Toggles](feature-toggles.md) diff --git a/docs/05-internals/design/principles/rdf-api.md b/docs/05-internals/design/principles/rdf-api.md index ca870afc27..7316e54501 100644 --- a/docs/05-internals/design/principles/rdf-api.md +++ b/docs/05-internals/design/principles/rdf-api.md @@ -5,14 +5,9 @@ # RDF Processing API -Knora provides an API for parsing and formatting RDF data and -for working with RDF graphs. This allows Knora developers to use a single, +DSP provides an API for parsing and formatting RDF data and +for working with RDF graphs. This allows DSP developers to use a single, idiomatic Scala API as a façade for a Java RDF library. -By using a feature toggle, you can choose either -[Jena](https://jena.apache.org/tutorials/rdf_api.html) -or -[RDF4J](https://rdf4j.org/documentation/programming/) -as the underlying implementation. ## Overview @@ -39,8 +34,8 @@ The API is in the package `org.knora.webapi.messages.util.rdf`. It includes: - `ShaclValidator`, which validates RDF models using SHACL shapes. To work with RDF models, start with `RdfFeatureFactory`, which returns instances -of `RdfNodeFactory`, `RdfModelFactory`, `RdfFormatUtil`, and `ShaclValidator`, -using feature toggle configuration. `JsonLDUtil` does not need a feature factory. +of `RdfNodeFactory`, `RdfModelFactory`, `RdfFormatUtil`, and `ShaclValidator`. +`JsonLDUtil` does not need a feature factory. To iterate efficiently over the statements in an `RdfModel`, use its `iterator` method. An `RdfModel` cannot be modified while you are iterating over it. @@ -93,20 +88,6 @@ Turtle file containing the graph of shapes. - The RDF4J-based implementation, in package `org.knora.webapi.messages.util.rdf.rdf4jimpl`. -## Feature toggle - -For an overview of feature toggles, see [Feature Toggles](feature-toggles.md). - -The RDF API uses the feature toggle `jena-rdf-library`: - -- `on`: use the Jena implementation. - -- `off` (the default): use the RDF4J implementation. - -The default setting is used on startup, e.g. to read ontologies from the -repository. After startup, the per-request setting is used. - - ## TODO - SHACL validation. diff --git a/dsp-shared/src/main/scala/dsp/errors/Errors.scala b/dsp-shared/src/main/scala/dsp/errors/Errors.scala index 847b3fe629..8d1b78a784 100644 --- a/dsp-shared/src/main/scala/dsp/errors/Errors.scala +++ b/dsp-shared/src/main/scala/dsp/errors/Errors.scala @@ -405,14 +405,6 @@ 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) - /** * Indicates that RDF processing failed. * diff --git a/mkdocs.yml b/mkdocs.yml index 505486de81..07bea41840 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,7 +21,6 @@ nav: - APIs: - The DSP APIs: - Index: 03-apis/index.md - - Feature Toggles: 03-apis/feature-toggles.md - API V1: - Index: 03-apis/api-v1/index.md - Introduction: 03-apis/api-v1/introduction.md @@ -80,7 +79,6 @@ nav: - Triplestore Updates: 05-internals/design/principles/triplestore-updates.md - Consistency Checking: 05-internals/design/principles/consistency-checking.md - Authentication: 05-internals/design/principles/authentication.md - - Feature Toggles: 05-internals/design/principles/feature-toggles.md - RDF Processing API: 05-internals/design/principles/rdf-api.md - API V1 Design: - Index: 05-internals/design/api-v1/index.md diff --git a/webapi/scripts/expected-client-test-data.txt b/webapi/scripts/expected-client-test-data.txt index 9e493ea9e0..900413910e 100644 --- a/webapi/scripts/expected-client-test-data.txt +++ b/webapi/scripts/expected-client-test-data.txt @@ -61,8 +61,6 @@ test-data/admin/lists/update-list-info-request.json test-data/admin/lists/update-list-info-response.json test-data/admin/lists/update-list-name-request.json test-data/admin/lists/update-list-name-response.json -test-data/admin/lists/update-node-info-name-request.json -test-data/admin/lists/update-node-info-name-response.json test-data/admin/lists/update-rootNode-comments-request.json test-data/admin/lists/update-rootNode-comments-response.json test-data/admin/lists/update-rootNode-labels-request.json diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala index 9f26a9191c..dae457ac7c 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFeatureFactory.scala @@ -14,10 +14,6 @@ import org.knora.webapi.settings.KnoraSettingsImpl */ object RdfFeatureFactory { - /** - * The name of the feature toggle that enables the Jena implementation of the RDF façade. - */ - // Jena singletons. private val jenaNodeFactory = new JenaNodeFactory private val jenaModelFactory = new JenaModelFactory(jenaNodeFactory) 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 34abae9711..e9182885fa 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/KnoraRoute.scala @@ -67,8 +67,6 @@ abstract class KnoraRouteFactory(routeData: KnoraRouteData) { * * - 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) - * * @return a route configured with the features enabled by the feature factory configuration. */ def makeRoute(): Route 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 1869104f4e..520f6c8305 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/SwaggerApiDocsRoute.scala @@ -23,8 +23,10 @@ class SwaggerApiDocsRoute(routeData: KnoraRouteData) extends KnoraRoute(routeDat // List all routes here override val apiClasses: Set[Class[_]] = Set( classOf[GroupsRouteADM], - classOf[OldListsRouteADMFeature], classOf[DeleteListItemsRouteADM], + classOf[CreateListItemsRouteADM], + classOf[GetListItemsRouteADM], + classOf[UpdateListItemsRouteADM], classOf[PermissionsRouteADM], classOf[ProjectsRouteADM], classOf[StoreRouteADM], 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 2ecdd5b1e7..c4e5a3e3ec 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 @@ -23,10 +23,6 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object GroupsRouteADM { - val GroupsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "groups") -} - /** * Provides a routing function for API routes that deal with groups. */ @@ -38,7 +34,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) with Authenticator with GroupsADMJsonProtocol { - import GroupsRouteADM._ + val groupsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "groups") override def makeRoute(): Route = getGroups() ~ @@ -52,7 +48,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) /** * Returns all groups. */ - private def getGroups(): Route = path(GroupsBasePath) { + private def getGroups(): Route = path(groupsBasePath) { get { requestContext => val requestMessage = for { _ <- getUserADM(requestContext) @@ -71,7 +67,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) /** * Returns a single group identified by IRI. */ - private def getGroup(): Route = path(GroupsBasePath / Segment) { value => + private def getGroup(): Route = path(groupsBasePath / Segment) { value => get { requestContext => val checkedGroupIri = stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid custom group IRI $value")) @@ -97,7 +93,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) * Returns all members of single group. */ private def getGroupMembers(): Route = - path(GroupsBasePath / Segment / "members") { value => + path(groupsBasePath / Segment / "members") { value => get { requestContext => val checkedGroupIri = stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid group IRI $value")) @@ -122,7 +118,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) /** * Creates a group. */ - private def createGroup(): Route = path(GroupsBasePath) { + private def createGroup(): Route = path(groupsBasePath) { post { entity(as[CreateGroupApiRequestADM]) { apiRequest => requestContext => val id: Validation[Throwable, Option[GroupIri]] = GroupIri.make(apiRequest.id) @@ -158,7 +154,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) /** * Updates basic group information. */ - private def updateGroup(): Route = path(GroupsBasePath / Segment) { value => + private def updateGroup(): Route = path(groupsBasePath / Segment) { value => put { entity(as[ChangeGroupApiRequestADM]) { apiRequest => requestContext => val checkedGroupIri = @@ -208,7 +204,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) * Updates the group's status. */ private def changeGroupStatus(): Route = - path(GroupsBasePath / Segment / "status") { value => + path(groupsBasePath / Segment / "status") { value => put { entity(as[ChangeGroupApiRequestADM]) { apiRequest => requestContext => val checkedGroupIri = @@ -248,7 +244,7 @@ class GroupsRouteADM(routeData: KnoraRouteData) /** * Deletes a group (sets status to false). */ - private def deleteGroup(): Route = path(GroupsBasePath / Segment) { value => + private def deleteGroup(): Route = path(groupsBasePath / Segment) { value => delete { requestContext => val checkedGroupIri = stringFormatter.validateAndEscapeIri(value, throw BadRequestException(s"Invalid group IRI $value")) 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 12a575be46..358f075029 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 @@ -16,12 +16,14 @@ import org.knora.webapi.routing.admin.lists._ * Provides an akka-http-routing function for API routes that deal with lists. */ class ListsRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) { - private val oldListRoute: OldListsRouteADMFeature = new OldListsRouteADMFeature(routeData) + private val getNodeRoute: GetListItemsRouteADM = new GetListItemsRouteADM(routeData) + private val createNodeRoute: CreateListItemsRouteADM = new CreateListItemsRouteADM(routeData) private val deleteNodeRoute: DeleteListItemsRouteADM = new DeleteListItemsRouteADM(routeData) private val updateNodeRoute: UpdateListItemsRouteADM = new UpdateListItemsRouteADM(routeData) override def makeRoute(): Route = - oldListRoute.makeRoute() ~ + getNodeRoute.makeRoute() ~ + createNodeRoute.makeRoute() ~ deleteNodeRoute.makeRoute() ~ updateNodeRoute.makeRoute() } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala index 49694d80b1..78d218e225 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 @@ -38,10 +38,6 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object ProjectsRouteADM { - val ProjectsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "projects") -} - @Api(value = "projects", produces = "application/json") @Path("/admin/projects") class ProjectsRouteADM(routeData: KnoraRouteData) @@ -49,7 +45,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) with Authenticator with ProjectsADMJsonProtocol { - import ProjectsRouteADM._ + val projectsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "projects") /** * Returns the route. @@ -87,7 +83,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) new ApiResponse(code = 500, message = "Internal server error") ) ) - private def getProjects(): Route = path(ProjectsBasePath) { + private def getProjects(): Route = path(projectsBasePath) { get { requestContext => log.info("All projects requested.") val requestMessage: Future[ProjectsGetRequestADM] = for { @@ -131,7 +127,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) new ApiResponse(code = 500, message = "Internal server error") ) ) - private def addProject(): Route = path(ProjectsBasePath) { + private def addProject(): Route = path(projectsBasePath) { post { entity(as[CreateProjectApiRequestADM]) { apiRequest => requestContext => // zio prelude: validation @@ -171,7 +167,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) } /* returns all unique keywords for all projects as a list */ - private def getKeywords(): Route = path(ProjectsBasePath / "Keywords") { + private def getKeywords(): Route = path(projectsBasePath / "Keywords") { get { requestContext => val requestMessage: Future[ProjectsKeywordsGetRequestADM] = for { requestingUser <- getUserADM( @@ -193,7 +189,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) /* returns all keywords for a single project */ private def getProjectKeywords(): Route = - path(ProjectsBasePath / "iri" / Segment / "Keywords") { value => + path(projectsBasePath / "iri" / Segment / "Keywords") { value => get { requestContext => val checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -221,7 +217,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) * returns a single project identified through iri */ private def getProjectByIri(): Route = - path(ProjectsBasePath / "iri" / Segment) { value => + path(projectsBasePath / "iri" / Segment) { value => get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( @@ -249,7 +245,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) * returns a single project identified through shortname. */ private def getProjectByShortname(): Route = - path(ProjectsBasePath / "shortname" / Segment) { value => + path(projectsBasePath / "shortname" / Segment) { value => get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( @@ -279,7 +275,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) * returns a single project identified through shortcode. */ private def getProjectByShortcode(): Route = - path(ProjectsBasePath / "shortcode" / Segment) { value => + path(projectsBasePath / "shortcode" / Segment) { value => get { requestContext => val requestMessage: Future[ProjectGetRequestADM] = for { requestingUser <- getUserADM( @@ -309,7 +305,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) * update a project identified by iri */ private def changeProject(): Route = - path(ProjectsBasePath / "iri" / Segment) { value => + path(projectsBasePath / "iri" / Segment) { value => put { entity(as[ChangeProjectApiRequestADM]) { apiRequest => requestContext => val checkedProjectIri = @@ -344,7 +340,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def deleteProject(): Route = - path(ProjectsBasePath / "iri" / Segment) { value => + path(projectsBasePath / "iri" / Segment) { value => delete { requestContext => val checkedProjectIri = stringFormatter.validateAndEscapeProjectIri(value, throw BadRequestException(s"Invalid project IRI $value")) @@ -375,7 +371,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectMembersByIri(): Route = - path(ProjectsBasePath / "iri" / Segment / "members") { value => + path(projectsBasePath / "iri" / Segment / "members") { value => get { requestContext => val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( @@ -404,7 +400,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectMembersByShortname(): Route = - path(ProjectsBasePath / "shortname" / Segment / "members") { value => + path(projectsBasePath / "shortname" / Segment / "members") { value => get { requestContext => val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( @@ -435,7 +431,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectMembersByShortcode(): Route = - path(ProjectsBasePath / "shortcode" / Segment / "members") { value => + path(projectsBasePath / "shortcode" / Segment / "members") { value => get { requestContext => val requestMessage: Future[ProjectMembersGetRequestADM] = for { requestingUser <- getUserADM( @@ -466,7 +462,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectAdminMembersByIri(): Route = - path(ProjectsBasePath / "iri" / Segment / "admin-members") { value => + path(projectsBasePath / "iri" / Segment / "admin-members") { value => get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( @@ -495,7 +491,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectAdminMembersByShortname(): Route = - path(ProjectsBasePath / "shortname" / Segment / "admin-members") { value => + path(projectsBasePath / "shortname" / Segment / "admin-members") { value => get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( @@ -526,7 +522,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectAdminMembersByShortcode(): Route = - path(ProjectsBasePath / "shortcode" / Segment / "admin-members") { value => + path(projectsBasePath / "shortcode" / Segment / "admin-members") { value => get { requestContext => val requestMessage: Future[ProjectAdminMembersGetRequestADM] = for { requestingUser <- getUserADM( @@ -557,7 +553,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectRestrictedViewSettingsByIri(): Route = - path(ProjectsBasePath / "iri" / Segment / "RestrictedViewSettings") { value: String => + path(projectsBasePath / "iri" / Segment / "RestrictedViewSettings") { value: String => get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( @@ -584,7 +580,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectRestrictedViewSettingsByShortname(): Route = - path(ProjectsBasePath / "shortname" / Segment / "RestrictedViewSettings") { value: String => + path(projectsBasePath / "shortname" / Segment / "RestrictedViewSettings") { value: String => get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( @@ -612,7 +608,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) */ @ApiMayChange private def getProjectRestrictedViewSettingsByShortcode(): Route = - path(ProjectsBasePath / "shortcode" / Segment / "RestrictedViewSettings") { value: String => + path(projectsBasePath / "shortcode" / Segment / "RestrictedViewSettings") { value: String => get { requestContext => val requestMessage: Future[ProjectRestrictedViewSettingsGetRequestADM] = for { requestingUser <- getUserADM( @@ -640,7 +636,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) * Returns all ontologies, data, and configuration belonging to a project. */ private def getProjectData(): Route = - path(ProjectsBasePath / "iri" / Segment / "AllData") { projectIri: IRI => + path(projectsBasePath / "iri" / Segment / "AllData") { projectIri: IRI => get { respondWithHeaders(projectDataHeader) { getProjectDataEntity( 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 5bf65d2ab0..cfd033b518 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 @@ -28,10 +28,6 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object UsersRouteADM { - val UsersBasePath: PathMatcher[Unit] = PathMatcher("admin" / "users") -} - /** * Provides an akka-http-routing function for API routes that deal with users. */ @@ -39,7 +35,7 @@ object UsersRouteADM { @Path("/admin/users") class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { - import UsersRouteADM._ + val usersBasePath: PathMatcher[Unit] = PathMatcher("admin" / "users") /** * Returns the route. @@ -71,7 +67,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) ) /* return all users */ - def getUsers(): Route = path(UsersBasePath) { + def getUsers(): Route = path(usersBasePath) { get { requestContext => val requestMessage: Future[UsersGetRequestADM] = for { requestingUser <- getUserADM( @@ -114,7 +110,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit ) ) /* create a new user */ - def addUser(): Route = path(UsersBasePath) { + def addUser(): Route = path(usersBasePath) { post { entity(as[CreateUserApiRequestADM]) { apiRequest => requestContext => // get all values from request and make value objects from it @@ -168,7 +164,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit /** * return a single user identified by iri */ - private def getUserByIri(): Route = path(UsersBasePath / "iri" / Segment) { userIri => + private def getUserByIri(): Route = path(usersBasePath / "iri" / Segment) { userIri => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( @@ -194,7 +190,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * return a single user identified by email */ private def getUserByEmail(): Route = - path(UsersBasePath / "email" / Segment) { userIri => + path(usersBasePath / "email" / Segment) { userIri => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( @@ -220,7 +216,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * return a single user identified by username */ private def getUserByUsername(): Route = - path(UsersBasePath / "username" / Segment) { userIri => + path(usersBasePath / "username" / Segment) { userIri => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( @@ -247,7 +243,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def changeUserBasicInformation(): Route = - path(UsersBasePath / "iri" / Segment / "BasicUserInformation") { userIri => + path(usersBasePath / "iri" / Segment / "BasicUserInformation") { userIri => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -325,7 +321,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def changeUserPassword(): Route = - path(UsersBasePath / "iri" / Segment / "Password") { userIri => + path(usersBasePath / "iri" / Segment / "Password") { userIri => put { entity(as[ChangeUserPasswordApiRequestADM]) { apiRequest => requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -377,7 +373,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def changeUserStatus(): Route = - path(UsersBasePath / "iri" / Segment / "Status") { userIri => + path(usersBasePath / "iri" / Segment / "Status") { userIri => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -424,7 +420,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit * API MAY CHANGE: delete a user identified by iri (change status to false). */ @ApiMayChange - private def deleteUser(): Route = path(UsersBasePath / "iri" / Segment) { userIri => + private def deleteUser(): Route = path(usersBasePath / "iri" / Segment) { userIri => delete { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -468,7 +464,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def changeUserSystemAdminMembership(): Route = - path(UsersBasePath / "iri" / Segment / "SystemAdmin") { userIri => + path(usersBasePath / "iri" / Segment / "SystemAdmin") { userIri => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -516,7 +512,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def getUsersProjectMemberships(): Route = - path(UsersBasePath / "iri" / Segment / "project-memberships") { userIri => + path(usersBasePath / "iri" / Segment / "project-memberships") { userIri => get { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -547,7 +543,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def addUserToProjectMembership(): Route = - path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => + path(usersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => post { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -594,7 +590,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def removeUserFromProjectMembership(): Route = - path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => + path(usersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => delete { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -641,7 +637,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def getUsersProjectAdminMemberships(): Route = - path(UsersBasePath / "iri" / Segment / "project-admin-memberships") { userIri => + path(usersBasePath / "iri" / Segment / "project-admin-memberships") { userIri => get { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -673,7 +669,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def addUserToProjectAdminMembership(): Route = - path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => + path(usersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => post { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -720,7 +716,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def removeUserFromProjectAdminMembership(): Route = - path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => + path(usersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => delete { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -767,7 +763,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def getUsersGroupMemberships(): Route = - path(UsersBasePath / "iri" / Segment / "group-memberships") { userIri => + path(usersBasePath / "iri" / Segment / "group-memberships") { userIri => get { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -798,7 +794,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def addUserToGroupMembership(): Route = - path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => + path(usersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => post { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") @@ -842,7 +838,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit */ @ApiMayChange private def removeUserFromGroupMembership(): Route = - path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => + path(usersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => delete { requestContext => if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala new file mode 100644 index 0000000000..0cb370d898 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala @@ -0,0 +1,196 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.routing.admin.lists + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.PathMatcher +import akka.http.scaladsl.server.Route +import io.swagger.annotations._ +import zio.prelude.Validation + +import java.util.UUID +import javax.ws.rs.Path +import scala.concurrent.Future + +import dsp.errors.BadRequestException +import dsp.errors.ForbiddenException +import dsp.valueobjects.Iri._ +import dsp.valueobjects.List._ +import dsp.valueobjects.ListErrorMessages +import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM +import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListRootNodeCreatePayloadADM +import org.knora.webapi.messages.admin.responder.listsmessages._ +import org.knora.webapi.routing.Authenticator +import org.knora.webapi.routing.KnoraRoute +import org.knora.webapi.routing.KnoraRouteData +import org.knora.webapi.routing.RouteUtilADM + +/** + * Provides routes to create list items. + * + * @param routeData the [[KnoraRouteData]] to be used in constructing the route. + */ +@Api(value = "lists", produces = "application/json") +@Path("/admin/lists") +class CreateListItemsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) + with Authenticator + with ListADMJsonProtocol { + + val listsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") + + def makeRoute(): Route = + createListRootNode() ~ + createListChildNode() + + @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[ListRootNodeCreateApiRequestADM], + paramType = "body" + ) + ) + ) + @ApiResponses( + Array( + new ApiResponse(code = 500, message = "Internal server error") + ) + ) + /** + * Creates a new list (root node). + */ + private def createListRootNode(): Route = path(listsBasePath) { + post { + entity(as[ListRootNodeCreateApiRequestADM]) { apiRequest => requestContext => + val maybeId: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.id) + val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.projectIri) + val validatedProjectIri: ProjectIri = ProjectIri + .make(apiRequest.projectIri) + .fold(e => throw e.head, v => v) + val maybeName: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) + val labels: Validation[Throwable, Labels] = Labels.make(apiRequest.labels) + val comments: Validation[Throwable, Comments] = Comments.make(apiRequest.comments) + val validatedListRootNodeCreatePayload: Validation[Throwable, ListRootNodeCreatePayloadADM] = + Validation.validateWith(maybeId, projectIri, maybeName, labels, comments)(ListRootNodeCreatePayloadADM) + + val requestMessage: Future[ListRootNodeCreateRequestADM] = for { + payload <- toFuture(validatedListRootNodeCreatePayload) + requestingUser <- getUserADM(requestContext) + + // check if the requesting user is allowed to perform operation + _ = + if ( + !requestingUser.permissions + .isProjectAdmin(validatedProjectIri.value) && !requestingUser.permissions.isSystemAdmin + ) { + // not project or a system admin + throw ForbiddenException(ListErrorMessages.ListCreatePermission) + } + } yield ListRootNodeCreateRequestADM( + createRootNode = payload, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + settings = settings, + appActor = appActor, + log = log + ) + } + } + } + + @Path("/{IRI}") + @ApiOperation( + value = "Add new node", + nickname = "addListNode", + httpMethod = "POST", + response = classOf[ChildNodeInfoGetResponseADM] + ) + @ApiImplicitParams( + Array( + new ApiImplicitParam( + name = "body", + value = "\"node\" to create", + required = true, + dataTypeClass = classOf[ListChildNodeCreateApiRequestADM], + paramType = "body" + ) + ) + ) + @ApiResponses( + Array( + new ApiResponse(code = 500, message = "Internal server error") + ) + ) + /** + * Creates a new list child node. + */ + private def createListChildNode(): Route = path(listsBasePath / Segment) { iri => + post { + entity(as[ListChildNodeCreateApiRequestADM]) { apiRequest => requestContext => + // check if requested ListIri matches the Iri passed in the route + val parentNodeIri: Validation[Throwable, ListIri] = if (iri == apiRequest.parentNodeIri) { + ListIri.make(apiRequest.parentNodeIri) + } else { + Validation.fail(throw BadRequestException("Route and payload parentNodeIri mismatch.")) + } + + val id: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.id) + val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.projectIri) + val validatedProjectIri: ProjectIri = projectIri.fold(e => throw e.head, v => v) + val name: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) + val position: Validation[Throwable, Option[Position]] = Position.make(apiRequest.position) + val labels: Validation[Throwable, Labels] = Labels.make(apiRequest.labels) + val comments: Validation[Throwable, Option[Comments]] = Comments.make(apiRequest.comments) + val validatedCreateChildNodePeyload: Validation[Throwable, ListChildNodeCreatePayloadADM] = + Validation.validateWith(id, parentNodeIri, projectIri, name, position, labels, comments)( + ListChildNodeCreatePayloadADM + ) + + val requestMessage: Future[ListChildNodeCreateRequestADM] = for { + payload <- toFuture(validatedCreateChildNodePeyload) + requestingUser <- getUserADM(requestContext) + + // check if the requesting user is allowed to perform operation + _ = + if ( + !requestingUser.permissions + .isProjectAdmin(validatedProjectIri.value) && !requestingUser.permissions.isSystemAdmin + ) { + // not project or a system admin + throw ForbiddenException(ListErrorMessages.ListCreatePermission) + } + } yield ListChildNodeCreateRequestADM( + createChildNodeRequest = payload, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + settings = settings, + appActor = appActor, + log = log + ) + } + } + } + +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala index 86c7719dbb..045a896654 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/DeleteListItemsRouteADM.scala @@ -19,12 +19,8 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object DeleteListItemsRouteADM { - val ListsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") -} - /** - * A [[Feature]] that provides routes to delete list items. + * Provides routes to delete list items. * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ @@ -33,7 +29,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) with Authenticator with ListADMJsonProtocol { - import DeleteListItemsRouteADM._ + val listsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") def makeRoute(): Route = deleteListItem() ~ @@ -41,7 +37,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) deleteListNodeComments() /* delete list (i.e. root node) or a child node which should also delete its children */ - private def deleteListItem(): Route = path(ListsBasePath / Segment) { iri => + private def deleteListItem(): Route = path(listsBasePath / Segment) { iri => delete { /* delete a list item root node or child if unused */ requestContext => @@ -70,7 +66,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) * Checks if a list can be deleted (none of its nodes is used in data). */ private def canDeleteList(): Route = - path(ListsBasePath / "candelete" / Segment) { iri => + path(listsBasePath / "candelete" / Segment) { iri => get { requestContext => val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list IRI: $iri")) @@ -96,7 +92,7 @@ class DeleteListItemsRouteADM(routeData: KnoraRouteData) * Deletes all comments from requested list node (only child). */ private def deleteListNodeComments(): Route = - path(ListsBasePath / "comments" / Segment) { iri => + path(listsBasePath / "comments" / Segment) { iri => delete { requestContext => val listIri = stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid list IRI: $iri")) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala new file mode 100644 index 0000000000..67d019e0ae --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/GetListItemsRouteADM.scala @@ -0,0 +1,168 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.routing.admin.lists + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.PathMatcher +import akka.http.scaladsl.server.Route +import io.swagger.annotations._ + +import javax.ws.rs.Path +import scala.concurrent.Future + +import dsp.errors.BadRequestException +import org.knora.webapi.IRI +import org.knora.webapi.messages.admin.responder.listsmessages._ +import org.knora.webapi.routing.Authenticator +import org.knora.webapi.routing.KnoraRoute +import org.knora.webapi.routing.KnoraRouteData +import org.knora.webapi.routing.RouteUtilADM + +/** + * Provides routes to get list items. + * + * @param routeData the [[KnoraRouteData]] to be used in constructing the route. + */ +@Api(value = "lists", produces = "application/json") +@Path("/admin/lists") +class GetListItemsRouteADM(routeData: KnoraRouteData) + extends KnoraRoute(routeData) + with Authenticator + with ListADMJsonProtocol { + + val listsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") + + def makeRoute(): Route = + getLists() ~ + getListNode() ~ + getListOrNodeInfo("infos") ~ + getListOrNodeInfo("nodes") ~ + getListInfo() + + @ApiOperation(value = "Get lists", nickname = "getlists", httpMethod = "GET", response = classOf[ListsGetResponseADM]) + @ApiResponses( + Array( + new ApiResponse(code = 500, message = "Internal server error") + ) + ) + /** + * Returns all lists optionally filtered by project. + */ + private def getLists(): Route = path(listsBasePath) { + get { + parameters("projectIri".?) { maybeProjectIri: Option[IRI] => requestContext => + val projectIri = + stringFormatter.validateAndEscapeOptionalIri( + maybeProjectIri, + throw BadRequestException(s"Invalid param project IRI: $maybeProjectIri") + ) + + val requestMessage: Future[ListsGetRequestADM] = for { + requestingUser <- getUserADM( + requestContext = requestContext + ) + } yield ListsGetRequestADM( + projectIri = projectIri, + requestingUser = requestingUser + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + settings = settings, + appActor = appActor, + log = log + ) + } + } + } + + @Path("/{IRI}") + @ApiOperation(value = "Get a list", nickname = "getlist", httpMethod = "GET", response = classOf[ListGetResponseADM]) + @ApiResponses( + Array( + new ApiResponse(code = 500, message = "Internal server error") + ) + ) + /** + * Returns a list node, root or child, with children (if exist). + */ + private def getListNode(): Route = path(listsBasePath / Segment) { iri => + get { requestContext => + val listIri = + stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListGetRequestADM] = for { + requestingUser <- getUserADM( + requestContext = requestContext + ) + } yield ListGetRequestADM( + iri = listIri, + requestingUser = requestingUser + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + settings = settings, + appActor = appActor, + log = log + ) + } + } + + /** + * Returns basic information about list node, root or child, w/o children (if exist). + */ + private def getListOrNodeInfo(routeSwitch: String): Route = + path(listsBasePath / routeSwitch / Segment) { iri => + get { requestContext => + val listIri = + stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + val requestMessage: Future[ListNodeInfoGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListNodeInfoGetRequestADM( + iri = listIri, + requestingUser = requestingUser + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + settings = settings, + appActor = appActor, + log = log + ) + } + } + + /** + * Returns basic information about a node, root or child, w/o children. + */ + private def getListInfo(): Route = + // Brought from new lists route implementation, has the e functionality as getListOrNodeInfo + path(listsBasePath / Segment / "info") { iri => + get { requestContext => + val listIri = + stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) + + val requestMessage: Future[ListNodeInfoGetRequestADM] = for { + requestingUser <- getUserADM(requestContext) + } yield ListNodeInfoGetRequestADM( + iri = listIri, + requestingUser = requestingUser + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + settings = settings, + appActor = appActor, + log = log + ) + } + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala deleted file mode 100644 index 7d15bc17e5..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/OldListsRouteADMFeature.scala +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.routing.admin.lists - -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.PathMatcher -import akka.http.scaladsl.server.Route -import io.swagger.annotations._ -import zio.prelude.Validation - -import java.util.UUID -import javax.ws.rs.Path -import scala.concurrent.Future - -import dsp.errors.BadRequestException -import dsp.errors.ForbiddenException -import dsp.valueobjects.Iri._ -import dsp.valueobjects.List._ -import dsp.valueobjects.ListErrorMessages -import org.knora.webapi.IRI -import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListChildNodeCreatePayloadADM -import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCreatePayloadADM.ListRootNodeCreatePayloadADM -import org.knora.webapi.messages.admin.responder.listsmessages._ -import org.knora.webapi.routing.Authenticator -import org.knora.webapi.routing.KnoraRoute -import org.knora.webapi.routing.KnoraRouteData -import org.knora.webapi.routing.RouteUtilADM - -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. - */ -@Api(value = "lists (old endpoint)", produces = "application/json") -@Path("/admin/lists") -class OldListsRouteADMFeature(routeData: KnoraRouteData) - extends KnoraRoute(routeData) - with Authenticator - with ListADMJsonProtocol { - - import OldListsRouteADMFeature._ - - def makeRoute(): Route = - getLists() ~ - getListNode() ~ - getListOrNodeInfo("infos") ~ - getListOrNodeInfo("nodes") ~ - getListInfo() ~ - createListRootNode() ~ - createListChildNode() ~ - updateList() - - @ApiOperation(value = "Get lists", nickname = "getlists", httpMethod = "GET", response = classOf[ListsGetResponseADM]) - @ApiResponses( - Array( - new ApiResponse(code = 500, message = "Internal server error") - ) - ) - /** - * Returns all lists optionally filtered by project. - */ - private def getLists(): Route = path(ListsBasePath) { - get { - parameters("projectIri".?) { maybeProjectIri: Option[IRI] => requestContext => - val projectIri = - stringFormatter.validateAndEscapeOptionalIri( - maybeProjectIri, - throw BadRequestException(s"Invalid param project IRI: $maybeProjectIri") - ) - - val requestMessage: Future[ListsGetRequestADM] = for { - requestingUser <- getUserADM( - requestContext = requestContext - ) - } yield ListsGetRequestADM( - projectIri = projectIri, - requestingUser = requestingUser - ) - - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - settings = settings, - appActor = appActor, - log = log - ) - } - } - } - - @Path("/{IRI}") - @ApiOperation(value = "Get a list", nickname = "getlist", httpMethod = "GET", response = classOf[ListGetResponseADM]) - @ApiResponses( - Array( - new ApiResponse(code = 500, message = "Internal server error") - ) - ) - /** - * Returns a list node, root or child, with children (if exist). - */ - private def getListNode(): Route = path(ListsBasePath / Segment) { iri => - get { requestContext => - val listIri = - stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - - val requestMessage: Future[ListGetRequestADM] = for { - requestingUser <- getUserADM( - requestContext = requestContext - ) - } yield ListGetRequestADM( - iri = listIri, - requestingUser = requestingUser - ) - - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - settings = settings, - appActor = appActor, - log = log - ) - } - } - - /** - * Returns basic information about list node, root or child, w/o children (if exist). - */ - private def getListOrNodeInfo(routeSwitch: String): Route = - path(ListsBasePath / routeSwitch / Segment) { iri => - get { requestContext => - val listIri = - stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - val requestMessage: Future[ListNodeInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListNodeInfoGetRequestADM( - iri = listIri, - requestingUser = requestingUser - ) - - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - settings = settings, - appActor = appActor, - log = log - ) - } - } - - /** - * Returns basic information about a node, root or child, w/o children. - */ - private def getListInfo(): Route = -// Brought from new lists route implementation, has the e functionality as getListOrNodeInfo - path(ListsBasePath / Segment / "info") { iri => - get { requestContext => - val listIri = - stringFormatter.validateAndEscapeIri(iri, throw BadRequestException(s"Invalid param list IRI: $iri")) - - val requestMessage: Future[ListNodeInfoGetRequestADM] = for { - requestingUser <- getUserADM(requestContext) - } yield ListNodeInfoGetRequestADM( - iri = listIri, - requestingUser = requestingUser - ) - - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - settings = settings, - appActor = appActor, - log = log - ) - } - } - - @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[ListRootNodeCreateApiRequestADM], - paramType = "body" - ) - ) - ) - @ApiResponses( - Array( - new ApiResponse(code = 500, message = "Internal server error") - ) - ) - /** - * Creates a new list (root node). - */ - private def createListRootNode(): Route = path(ListsBasePath) { - post { - entity(as[ListRootNodeCreateApiRequestADM]) { apiRequest => requestContext => - val maybeId: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.id) - val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.projectIri) - val maybeName: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) - val labels: Validation[Throwable, Labels] = Labels.make(apiRequest.labels) - val comments: Validation[Throwable, Comments] = Comments.make(apiRequest.comments) - val validatedListRootNodeCreatePayload: Validation[Throwable, ListRootNodeCreatePayloadADM] = - Validation.validateWith(maybeId, projectIri, maybeName, labels, comments)(ListRootNodeCreatePayloadADM) - - val requestMessage: Future[ListRootNodeCreateRequestADM] = for { - payload <- toFuture(validatedListRootNodeCreatePayload) - requestingUser <- getUserADM(requestContext) - - // check if the requesting user is allowed to perform operation - _ = if ( - !requestingUser.permissions.isProjectAdmin( - projectIri.toOption.get.value - ) && !requestingUser.permissions.isSystemAdmin - ) { - // not project or a system admin - throw ForbiddenException(ListErrorMessages.ListCreatePermission) - } - } yield ListRootNodeCreateRequestADM( - createRootNode = payload, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) - - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - settings = settings, - appActor = appActor, - log = log - ) - } - } - } - - @Path("/{IRI}") - @ApiOperation( - value = "Add new node", - nickname = "addListNode", - httpMethod = "POST", - response = classOf[ChildNodeInfoGetResponseADM] - ) - @ApiImplicitParams( - Array( - new ApiImplicitParam( - name = "body", - value = "\"node\" to create", - required = true, - dataTypeClass = classOf[ListChildNodeCreateApiRequestADM], - paramType = "body" - ) - ) - ) - @ApiResponses( - Array( - new ApiResponse(code = 500, message = "Internal server error") - ) - ) - /** - * Creates a new list child node. - */ - private def createListChildNode(): Route = path(ListsBasePath / Segment) { iri => - post { - entity(as[ListChildNodeCreateApiRequestADM]) { apiRequest => requestContext => - // check if requested ListIri matches the Iri passed in the route - val parentNodeIri: Validation[Throwable, ListIri] = if (iri == apiRequest.parentNodeIri) { - ListIri.make(apiRequest.parentNodeIri) - } else { - Validation.fail(throw BadRequestException("Route and payload parentNodeIri mismatch.")) - } - - val id: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.id) - val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.projectIri) - val name: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) - val position: Validation[Throwable, Option[Position]] = Position.make(apiRequest.position) - val labels: Validation[Throwable, Labels] = Labels.make(apiRequest.labels) - val comments: Validation[Throwable, Option[Comments]] = Comments.make(apiRequest.comments) - val validatedCreateChildNodePeyload: Validation[Throwable, ListChildNodeCreatePayloadADM] = - Validation.validateWith(id, parentNodeIri, projectIri, name, position, labels, comments)( - ListChildNodeCreatePayloadADM - ) - - val requestMessage: Future[ListChildNodeCreateRequestADM] = for { - payload <- toFuture(validatedCreateChildNodePeyload) - requestingUser <- getUserADM(requestContext) - - // check if the requesting user is allowed to perform operation - _ = if ( - !requestingUser.permissions.isProjectAdmin( - projectIri.toOption.get.value - ) && !requestingUser.permissions.isSystemAdmin - ) { - // not project or a system admin - throw ForbiddenException(ListErrorMessages.ListCreatePermission) - } - } yield ListChildNodeCreateRequestADM( - createChildNodeRequest = payload, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) - - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - settings = settings, - appActor = appActor, - log = log - ) - } - } - } - - @Path("/{IRI}") - @ApiOperation( - value = "Update basic list information", - nickname = "putList", - httpMethod = "PUT", - response = classOf[RootNodeInfoGetResponseADM] - ) - @ApiImplicitParams( - Array( - new ApiImplicitParam( - name = "body", - value = "\"list\" to update", - required = true, - dataTypeClass = classOf[ListNodeChangeApiRequestADM], - paramType = "body" - ) - ) - ) - @ApiResponses( - Array( - new ApiResponse(code = 500, message = "Internal server error") - ) - ) - /** - * Updates existing list node, either root or child. - */ - private def updateList(): Route = path(ListsBasePath / Segment) { iri => - put { - entity(as[ListNodeChangeApiRequestADM]) { apiRequest => requestContext => - // check if requested Iri matches the route Iri - val listIri: Validation[Throwable, ListIri] = if (iri == apiRequest.listIri) { - ListIri.make(apiRequest.listIri) - } else { - Validation.fail(throw BadRequestException("Route and payload listIri mismatch.")) - } - - val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.projectIri) - val hasRootNode: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.hasRootNode) - val position: Validation[Throwable, Option[Position]] = Position.make(apiRequest.position) - val name: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) - val labels: Validation[Throwable, Option[Labels]] = Labels.make(apiRequest.labels) - val comments: Validation[Throwable, Option[Comments]] = Comments.make(apiRequest.comments) - - val validatedChangeNodeInfoPayload: Validation[Throwable, ListNodeChangePayloadADM] = - Validation.validateWith(listIri, projectIri, hasRootNode, position, name, labels, comments)( - ListNodeChangePayloadADM - ) - - val requestMessage: Future[NodeInfoChangeRequestADM] = for { - payload <- toFuture(validatedChangeNodeInfoPayload) - requestingUser <- getUserADM(requestContext) - // check if the requesting user is allowed to perform operation - _ = if ( - !requestingUser.permissions.isProjectAdmin( - projectIri.toOption.get.value - ) && !requestingUser.permissions.isSystemAdmin - ) { - // not project or a system admin - throw ForbiddenException(ListErrorMessages.ListNodeCreatePermission) - } - } yield NodeInfoChangeRequestADM( - listIri = listIri.toOption.get.value, - changeNodeRequest = payload, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) - - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - settings = settings, - appActor = appActor, - log = log - ) - } - } - } -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala index dd3c4e3041..4be4c0209a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala @@ -9,25 +9,25 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.PathMatcher import akka.http.scaladsl.server.Route import io.swagger.annotations._ +import zio.prelude.Validation import java.util.UUID import javax.ws.rs.Path import scala.concurrent.Future import dsp.errors.BadRequestException +import dsp.errors.ForbiddenException +import dsp.valueobjects.Iri._ import dsp.valueobjects.List._ +import dsp.valueobjects.ListErrorMessages import org.knora.webapi.messages.admin.responder.listsmessages._ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object UpdateListItemsRouteADM { - val ListsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") -} - /** - * A [[Feature]] that provides routes to delete list items. + * Provides routes to update list items. * * @param routeData the [[KnoraRouteData]] to be used in constructing the route. */ @@ -36,13 +36,14 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) with Authenticator with ListADMJsonProtocol { - import UpdateListItemsRouteADM._ + val listsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "lists") def makeRoute(): Route = updateNodeName() ~ updateNodeLabels() ~ updateNodeComments() ~ - updateNodePosition() + updateNodePosition() ~ + updateList() @Path("/{IRI}/name") @ApiOperation( @@ -71,7 +72,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) * Update name of an existing list node, either root or child. */ private def updateNodeName(): Route = - path(ListsBasePath / Segment / "name") { iri => + path(listsBasePath / Segment / "name") { iri => put { entity(as[ChangeNodeNameApiRequestADM]) { apiRequest => requestContext => val nodeIri = @@ -127,7 +128,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) * Update labels of an existing list node, either root or child. */ private def updateNodeLabels(): Route = - path(ListsBasePath / Segment / "labels") { iri => + path(listsBasePath / Segment / "labels") { iri => put { entity(as[ChangeNodeLabelsApiRequestADM]) { apiRequest => requestContext => val nodeIri = @@ -183,7 +184,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) * Updates comments of an existing list node, either root or child. */ private def updateNodeComments(): Route = - path(ListsBasePath / Segment / "comments") { iri => + path(listsBasePath / Segment / "comments") { iri => put { entity(as[ChangeNodeCommentsApiRequestADM]) { apiRequest => requestContext => val nodeIri = @@ -239,7 +240,7 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) * Updates position of an existing list child node. */ private def updateNodePosition(): Route = - path(ListsBasePath / Segment / "position") { iri => + path(listsBasePath / Segment / "position") { iri => put { entity(as[ChangeNodePositionApiRequestADM]) { apiRequest => requestContext => val nodeIri = @@ -264,4 +265,84 @@ class UpdateListItemsRouteADM(routeData: KnoraRouteData) } } } + + @Path("/{IRI}") + @ApiOperation( + value = "Update basic list information", + nickname = "putList", + httpMethod = "PUT", + response = classOf[RootNodeInfoGetResponseADM] + ) + @ApiImplicitParams( + Array( + new ApiImplicitParam( + name = "body", + value = "\"list\" to update", + required = true, + dataTypeClass = classOf[ListNodeChangeApiRequestADM], + paramType = "body" + ) + ) + ) + @ApiResponses( + Array( + new ApiResponse(code = 500, message = "Internal server error") + ) + ) + /** + * Updates existing list node, either root or child. + */ + private def updateList(): Route = path(listsBasePath / Segment) { iri => + put { + entity(as[ListNodeChangeApiRequestADM]) { apiRequest => requestContext => + // check if requested Iri matches the route Iri + val listIri: Validation[Throwable, ListIri] = if (iri == apiRequest.listIri) { + ListIri.make(apiRequest.listIri) + } else { + Validation.fail(throw BadRequestException("Route and payload listIri mismatch.")) + } + val validatedListIri: ListIri = listIri.fold(e => throw e.head, v => v) + + val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.projectIri) + val validatedProjectIri: ProjectIri = projectIri.fold(e => throw e.head, v => v) + val hasRootNode: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.hasRootNode) + val position: Validation[Throwable, Option[Position]] = Position.make(apiRequest.position) + val name: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) + val labels: Validation[Throwable, Option[Labels]] = Labels.make(apiRequest.labels) + val comments: Validation[Throwable, Option[Comments]] = Comments.make(apiRequest.comments) + + val validatedChangeNodeInfoPayload: Validation[Throwable, ListNodeChangePayloadADM] = + Validation.validateWith(listIri, projectIri, hasRootNode, position, name, labels, comments)( + ListNodeChangePayloadADM + ) + + val requestMessage: Future[NodeInfoChangeRequestADM] = for { + payload <- toFuture(validatedChangeNodeInfoPayload) + requestingUser <- getUserADM(requestContext) + // check if the requesting user is allowed to perform operation + _ = if ( + !requestingUser.permissions.isProjectAdmin( + validatedProjectIri.value + ) && !requestingUser.permissions.isSystemAdmin + ) { + // not project or a system admin + throw ForbiddenException(ListErrorMessages.ListNodeCreatePermission) + } + } yield NodeInfoChangeRequestADM( + listIri = validatedListIri.value, + changeNodeRequest = payload, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + settings = settings, + appActor = appActor, + log = log + ) + } + } + } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala index 32dd4659ab..58e8a4c633 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/CreatePermissionRouteADM.scala @@ -20,10 +20,6 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object CreatePermissionRouteADM { - val PermissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") -} - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") class CreatePermissionRouteADM(routeData: KnoraRouteData) @@ -31,7 +27,7 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData) with Authenticator with PermissionsADMJsonProtocol { - import CreatePermissionRouteADM._ + val permissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") /** * Returns the route. @@ -44,7 +40,7 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData) * Create a new administrative permission */ private def createAdministrativePermission(): Route = - path(PermissionsBasePath / "ap") { + path(permissionsBasePath / "ap") { post { /* create a new administrative permission */ entity(as[CreateAdministrativePermissionAPIRequestADM]) { apiRequest => requestContext => @@ -71,7 +67,7 @@ class CreatePermissionRouteADM(routeData: KnoraRouteData) * Create default object access permission */ private def createDefaultObjectAccessPermission(): Route = - path(PermissionsBasePath / "doap") { + path(permissionsBasePath / "doap") { post { /* create a new default object access permission */ entity(as[CreateDefaultObjectAccessPermissionAPIRequestADM]) { apiRequest => requestContext => diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala index e5620941cf..dcb87ebe90 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/DeletePermissionRouteADM.scala @@ -19,10 +19,6 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object DeletePermissionRouteADM { - val PermissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") -} - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") class DeletePermissionRouteADM(routeData: KnoraRouteData) @@ -30,7 +26,7 @@ class DeletePermissionRouteADM(routeData: KnoraRouteData) with Authenticator with PermissionsADMJsonProtocol { - import DeletePermissionRouteADM._ + val permissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") /** * Returns the route. @@ -42,7 +38,7 @@ class DeletePermissionRouteADM(routeData: KnoraRouteData) * Delete a permission */ private def deletePermission(): Route = - path(PermissionsBasePath / Segment) { iri => + path(permissionsBasePath / Segment) { iri => delete { requestContext => val requestMessage = for { requestingUser <- getUserADM(requestContext) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala index 12490567a4..2596243508 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/GetPermissionsRouteADM.scala @@ -19,10 +19,6 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object GetPermissionsRouteADM { - val PermissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") -} - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") class GetPermissionsRouteADM(routeData: KnoraRouteData) @@ -30,7 +26,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) with Authenticator with PermissionsADMJsonProtocol { - import GetPermissionsRouteADM._ + val permissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") /** * Returns the route. @@ -42,7 +38,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) getPermissionsForProject() private def getAdministrativePermissionForProjectGroup(): Route = - path(PermissionsBasePath / "ap" / Segment / Segment) { (projectIri, groupIri) => + path(permissionsBasePath / "ap" / Segment / Segment) { (projectIri, groupIri) => get { requestContext => val requestMessage = for { requestingUser <- getUserADM(requestContext) @@ -59,7 +55,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) } private def getAdministrativePermissionsForProject(): Route = - path(PermissionsBasePath / "ap" / Segment) { projectIri => + path(permissionsBasePath / "ap" / Segment) { projectIri => get { requestContext => val requestMessage = for { requestingUser <- getUserADM(requestContext) @@ -80,7 +76,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) } private def getDefaultObjectAccessPermissionsForProject(): Route = - path(PermissionsBasePath / "doap" / Segment) { projectIri => + path(permissionsBasePath / "doap" / Segment) { projectIri => get { requestContext => val requestMessage = for { requestingUser <- getUserADM(requestContext) @@ -101,7 +97,7 @@ class GetPermissionsRouteADM(routeData: KnoraRouteData) } private def getPermissionsForProject(): Route = - path(PermissionsBasePath / Segment) { projectIri => + path(permissionsBasePath / Segment) { projectIri => get { requestContext => val requestMessage = for { requestingUser <- getUserADM(requestContext) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala index f92c56ab4f..9fc30947e2 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/permissions/UpdatePermissionRouteADM.scala @@ -20,10 +20,6 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM -object UpdatePermissionRouteADM { - val PermissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") -} - @Api(value = "permissions", produces = "application/json") @Path("/admin/permissions") class UpdatePermissionRouteADM(routeData: KnoraRouteData) @@ -31,7 +27,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) with Authenticator with PermissionsADMJsonProtocol { - import UpdatePermissionRouteADM._ + val permissionsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "permissions") /** * Returns the route. @@ -46,7 +42,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) * Update a permission's group */ private def updatePermissionGroup(): Route = - path(PermissionsBasePath / Segment / "group") { iri => + path(permissionsBasePath / Segment / "group") { iri => put { entity(as[ChangePermissionGroupApiRequestADM]) { apiRequest => requestContext => val permissionIri = @@ -76,7 +72,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) * Update a permission's set of hasPermissions. */ private def updatePermissionHasPermissions(): Route = - path(PermissionsBasePath / Segment / "hasPermissions") { iri => + path(permissionsBasePath / Segment / "hasPermissions") { iri => put { entity(as[ChangePermissionHasPermissionsApiRequestADM]) { apiRequest => requestContext => val permissionIri = @@ -106,7 +102,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) * Update a doap permission by setting it for a new resource class */ private def updatePermissionResourceClass(): Route = - path(PermissionsBasePath / Segment / "resourceClass") { iri => + path(permissionsBasePath / Segment / "resourceClass") { iri => put { entity(as[ChangePermissionResourceClassApiRequestADM]) { apiRequest => requestContext => val permissionIri = @@ -136,7 +132,7 @@ class UpdatePermissionRouteADM(routeData: KnoraRouteData) * Update a doap permission by setting it for a new property class */ private def updatePermissionProperty(): Route = - path(PermissionsBasePath / Segment / "property") { iri => + path(permissionsBasePath / Segment / "property") { iri => put { entity(as[ChangePermissionPropertyApiRequestADM]) { apiRequest => requestContext => val permissionIri = 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 d6c62250e4..cf57663848 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 @@ -37,16 +37,12 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilV2 -object OntologiesRouteV2 { - val OntologiesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "ontologies") -} - /** * Provides a routing function for API v2 routes that deal with ontologies. */ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { - import OntologiesRouteV2._ + val ontologiesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "ontologies") private val ALL_LANGUAGES = "allLanguages" private val LAST_MODIFICATION_DATE = "lastModificationDate" @@ -143,7 +139,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getOntologyMetadata(): Route = - path(OntologiesBasePath / "metadata") { + path(ontologiesBasePath / "metadata") { get { requestContext => val maybeProjectIri: Option[SmartIri] = RouteUtilV2.getProject(requestContext) @@ -169,7 +165,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def updateOntologyMetadata(): Route = - path(OntologiesBasePath / "metadata") { + path(ontologiesBasePath / "metadata") { put { entity(as[String]) { jsonRequest => requestContext => { @@ -205,7 +201,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getOntologyMetadataForProjects(): Route = - path(OntologiesBasePath / "metadata" / Segments) { projectIris: List[IRI] => + path(ontologiesBasePath / "metadata" / Segments) { projectIris: List[IRI] => get { requestContext => val requestMessageFuture: Future[OntologyMetadataGetByProjectRequestV2] = for { requestingUser <- getUserADM( @@ -234,7 +230,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getOntology(): Route = - path(OntologiesBasePath / "allentities" / Segment) { externalOntologyIriStr: IRI => + path(ontologiesBasePath / "allentities" / Segment) { externalOntologyIriStr: IRI => get { requestContext => val requestedOntologyIri = externalOntologyIriStr.toSmartIriWithErr( throw BadRequestException(s"Invalid ontology IRI: $externalOntologyIriStr") @@ -276,7 +272,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def createClass(): Route = path(OntologiesBasePath / "classes") { + private def createClass(): Route = path(ontologiesBasePath / "classes") { post { // Create a new class. entity(as[String]) { jsonRequest => requestContext => @@ -313,7 +309,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def updateClass(): Route = - path(OntologiesBasePath / "classes") { + path(ontologiesBasePath / "classes") { put { // Change the labels or comments of a class. entity(as[String]) { jsonRequest => requestContext => @@ -351,7 +347,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) // delete the comment of a class definition private def deleteClassComment(): Route = - path(OntologiesBasePath / "classes" / "comment" / Segment) { classIriStr: IRI => + path(ontologiesBasePath / "classes" / "comment" / Segment) { classIriStr: IRI => delete { requestContext => val classIri = classIriStr.toSmartIri @@ -393,7 +389,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def addCardinalities(): Route = - path(OntologiesBasePath / "cardinalities") { + path(ontologiesBasePath / "cardinalities") { post { // Add cardinalities to a class. entity(as[String]) { jsonRequest => requestContext => @@ -430,7 +426,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def canReplaceCardinalities(): Route = - path(OntologiesBasePath / "canreplacecardinalities" / Segment) { classIriStr: IRI => + path(ontologiesBasePath / "canreplacecardinalities" / Segment) { classIriStr: IRI => get { requestContext => val classIri = classIriStr.toSmartIri stringFormatter.checkExternalOntologyName(classIri) @@ -463,7 +459,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) // Replaces all cardinalities with what was sent. Deleting means send empty // replace request. private def replaceCardinalities(): Route = - path(OntologiesBasePath / "cardinalities") { + path(ontologiesBasePath / "cardinalities") { put { entity(as[String]) { jsonRequest => requestContext => { @@ -499,7 +495,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def canDeleteCardinalitiesFromClass(): Route = - path(OntologiesBasePath / "candeletecardinalities") { + path(ontologiesBasePath / "candeletecardinalities") { post { entity(as[String]) { jsonRequest => requestContext => { @@ -538,7 +534,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) // delete a single cardinality from the specified class if the property is // not used in resources. private def deleteCardinalitiesFromClass(): Route = - path(OntologiesBasePath / "cardinalities") { + path(ontologiesBasePath / "cardinalities") { patch { entity(as[String]) { jsonRequest => requestContext => { @@ -574,7 +570,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def changeGuiOrder(): Route = - path(OntologiesBasePath / "guiorder") { + path(ontologiesBasePath / "guiorder") { put { // Change a class's cardinalities. entity(as[String]) { jsonRequest => requestContext => @@ -611,7 +607,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getClasses(): Route = - path(OntologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => + path(ontologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => get { requestContext => val classesAndSchemas: Set[(SmartIri, ApiV2Schema)] = externalResourceClassIris.map { classIriStr: IRI => val requestedClassIri: SmartIri = @@ -675,7 +671,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def canDeleteClass(): Route = - path(OntologiesBasePath / "candeleteclass" / Segment) { classIriStr: IRI => + path(ontologiesBasePath / "candeleteclass" / Segment) { classIriStr: IRI => get { requestContext => val classIri = classIriStr.toSmartIri stringFormatter.checkExternalOntologyName(classIri) @@ -706,7 +702,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def deleteClass(): Route = - path(OntologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => + path(ontologiesBasePath / "classes" / Segments) { externalResourceClassIris: List[IRI] => delete { requestContext => val classIriStr = externalResourceClassIris match { case List(str) => str @@ -753,7 +749,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def deleteOntologyComment(): Route = - path(OntologiesBasePath / "comment" / Segment) { ontologyIriStr: IRI => + path(ontologiesBasePath / "comment" / Segment) { ontologyIriStr: IRI => delete { requestContext => val ontologyIri = ontologyIriStr.toSmartIri @@ -795,7 +791,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def createProperty(): Route = - path(OntologiesBasePath / "properties") { + path(ontologiesBasePath / "properties") { post { // Create a new property. entity(as[String]) { jsonRequest => requestContext => @@ -1008,7 +1004,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def updatePropertyLabelsOrComments(): Route = - path(OntologiesBasePath / "properties") { + path(ontologiesBasePath / "properties") { put { // Change the labels or comments of a property. entity(as[String]) { jsonRequest => requestContext => @@ -1047,7 +1043,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) // delete the comment of a property definition private def deletePropertyComment(): Route = - path(OntologiesBasePath / "properties" / "comment" / Segment) { propertyIriStr: IRI => + path(ontologiesBasePath / "properties" / "comment" / Segment) { propertyIriStr: IRI => delete { requestContext => val propertyIri = propertyIriStr.toSmartIri @@ -1089,7 +1085,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def updatePropertyGuiElement(): Route = - path(OntologiesBasePath / "properties" / "guielement") { + path(ontologiesBasePath / "properties" / "guielement") { put { // Change the salsah-gui:guiElement and/or salsah-gui:guiAttribute of a property. entity(as[String]) { jsonRequest => requestContext => @@ -1171,7 +1167,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getProperties(): Route = - path(OntologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => + path(ontologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => get { requestContext => val propsAndSchemas: Set[(SmartIri, ApiV2Schema)] = externalPropertyIris.map { propIriStr: IRI => val requestedPropIri: SmartIri = @@ -1235,7 +1231,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def canDeleteProperty(): Route = - path(OntologiesBasePath / "candeleteproperty" / Segment) { propertyIriStr: IRI => + path(ontologiesBasePath / "candeleteproperty" / Segment) { propertyIriStr: IRI => get { requestContext => val propertyIri = propertyIriStr.toSmartIri stringFormatter.checkExternalOntologyName(propertyIri) @@ -1266,7 +1262,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def deleteProperty(): Route = - path(OntologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => + path(ontologiesBasePath / "properties" / Segments) { externalPropertyIris: List[IRI] => delete { requestContext => val propertyIriStr = externalPropertyIris match { case List(str) => str @@ -1312,7 +1308,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def createOntology(): Route = path(OntologiesBasePath) { + private def createOntology(): Route = path(ontologiesBasePath) { // Create a new, empty ontology. post { entity(as[String]) { jsonRequest => requestContext => @@ -1349,7 +1345,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def canDeleteOntology(): Route = - path(OntologiesBasePath / "candeleteontology" / Segment) { ontologyIriStr: IRI => + path(ontologiesBasePath / "candeleteontology" / Segment) { ontologyIriStr: IRI => get { requestContext => val ontologyIri = ontologyIriStr.toSmartIri stringFormatter.checkExternalOntologyName(ontologyIri) @@ -1379,7 +1375,7 @@ class OntologiesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def deleteOntology(): Route = path(OntologiesBasePath / Segment) { ontologyIriStr => + private def deleteOntology(): Route = path(ontologiesBasePath / Segment) { ontologyIriStr => delete { requestContext => val ontologyIri = ontologyIriStr.toSmartIri stringFormatter.checkExternalOntologyName(ontologyIri) 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 63c8127da3..ee47940ce5 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 @@ -27,16 +27,12 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilV2 -object ResourcesRouteV2 { - val ResourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") -} - /** * Provides a routing function for API v2 routes that deal with resources. */ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { - import ResourcesRouteV2._ + val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources") private val Text_Property = "textProperty" private val Mapping_Iri = "mappingIri" @@ -68,7 +64,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) eraseResource() private def getIIIFManifest(): Route = - path(ResourcesBasePath / "iiifmanifest" / Segment) { resourceIriStr: IRI => + path(resourcesBasePath / "iiifmanifest" / Segment) { resourceIriStr: IRI => get { requestContext => val resourceIri: IRI = stringFormatter.validateAndEscapeIri( @@ -97,7 +93,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def createResource(): Route = path(ResourcesBasePath) { + private def createResource(): Route = path(resourcesBasePath) { post { entity(as[String]) { jsonRequest => requestContext => { @@ -132,7 +128,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def updateResourceMetadata(): Route = path(ResourcesBasePath) { + private def updateResourceMetadata(): Route = path(resourcesBasePath) { put { entity(as[String]) { jsonRequest => requestContext => { @@ -167,7 +163,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResourcesInProject(): Route = path(ResourcesBasePath) { + private def getResourcesInProject(): Route = path(resourcesBasePath) { get { requestContext => val projectIri: SmartIri = RouteUtilV2 .getProject(requestContext) @@ -234,7 +230,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getResourceHistory(): Route = - path(ResourcesBasePath / "history" / Segment) { resourceIriStr: IRI => + path(resourcesBasePath / "history" / Segment) { resourceIriStr: IRI => get { requestContext => val resourceIri = stringFormatter.validateAndEscapeIri( @@ -278,7 +274,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getResourceHistoryEvents(): Route = - path(ResourcesBasePath / "resourceHistoryEvents" / Segment) { resourceIri: IRI => + path(resourcesBasePath / "resourceHistoryEvents" / Segment) { resourceIri: IRI => get { requestContext => val requestMessageFuture: Future[ResourceHistoryEventsGetRequestV2] = for { requestingUser <- getUserADM( @@ -302,7 +298,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } private def getProjectResourceAndValueHistory(): Route = - path(ResourcesBasePath / "projectHistoryEvents" / Segment) { projectIri: IRI => + path(resourcesBasePath / "projectHistoryEvents" / Segment) { projectIri: IRI => get { requestContext => val requestMessageFuture: Future[ProjectResourcesWithHistoryGetRequestV2] = for { requestingUser <- getUserADM( @@ -325,7 +321,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def getResources(): Route = path(ResourcesBasePath / Segments) { resIris: Seq[String] => + private def getResources(): Route = path(resourcesBasePath / Segments) { resIris: Seq[String] => get { requestContext => if (resIris.size > settings.v2ResultsPerPage) throw BadRequestException(s"List of provided resource Iris exceeds limit of ${settings.v2ResultsPerPage}") @@ -507,7 +503,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def deleteResource(): Route = path(ResourcesBasePath / "delete") { + private def deleteResource(): Route = path(resourcesBasePath / "delete") { post { entity(as[String]) { jsonRequest => requestContext => { @@ -541,7 +537,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) } } - private def eraseResource(): Route = path(ResourcesBasePath / "erase") { + private def eraseResource(): Route = path(resourcesBasePath / "erase") { post { entity(as[String]) { jsonRequest => requestContext => { 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 19a928dd92..bb76000984 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,16 +26,12 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilV2 -object ValuesRouteV2 { - val ValuesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "values") -} - /** * Provides a routing function for API v2 routes that deal with values. */ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { - import ValuesRouteV2._ + val valuesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "values") /** * Returns the route. @@ -46,7 +42,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit updateValue() ~ deleteValue() - private def getValue(): Route = path(ValuesBasePath / Segment / Segment) { + private def getValue(): Route = path(valuesBasePath / Segment / Segment) { (resourceIriStr: IRI, valueUuidStr: String) => get { requestContext => val resourceIri: SmartIri = @@ -104,7 +100,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def createValue(): Route = path(ValuesBasePath) { + private def createValue(): Route = path(valuesBasePath) { post { entity(as[String]) { jsonRequest => requestContext => { @@ -138,7 +134,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def updateValue(): Route = path(ValuesBasePath) { + private def updateValue(): Route = path(valuesBasePath) { put { entity(as[String]) { jsonRequest => requestContext => { @@ -172,7 +168,7 @@ class ValuesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - private def deleteValue(): Route = path(ValuesBasePath / "delete") { + private def deleteValue(): Route = path(valuesBasePath / "delete") { post { entity(as[String]) { jsonRequest => requestContext => { diff --git a/webapi/src/test/resources/logback-test.xml b/webapi/src/test/resources/logback-test.xml index f9f996aed3..c9b19feafb 100644 --- a/webapi/src/test/resources/logback-test.xml +++ b/webapi/src/test/resources/logback-test.xml @@ -134,7 +134,7 @@ - + diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/CreateListItemsRouteADME2ESpec.scala similarity index 52% rename from webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala rename to webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/CreateListItemsRouteADME2ESpec.scala index 140e313968..69a051a275 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/CreateListItemsRouteADME2ESpec.scala @@ -7,7 +7,6 @@ package org.knora.webapi.e2e.admin.lists import akka.actor.ActorSystem import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.testkit.RouteTestTimeout import akka.http.scaladsl.unmarshalling.Unmarshal import com.typesafe.config.Config @@ -27,12 +26,11 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol import org.knora.webapi.messages.v1.responder.sessionmessages.SessionJsonProtocol import org.knora.webapi.messages.v1.routing.authenticationmessages.CredentialsADM -import org.knora.webapi.sharedtestdata.SharedListsTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.util.AkkaHttpUtils import org.knora.webapi.util.MutableTestIri -object OldListsRouteADMFeatureE2ESpec { +object CreateListItemsRouteADME2ESpec { val config: Config = ConfigFactory.parseString(""" akka.loglevel = "DEBUG" akka.stdout-loglevel = "DEBUG" @@ -42,8 +40,8 @@ object OldListsRouteADMFeatureE2ESpec { /** * End-to-End (E2E) test specification for testing lists endpoint. */ -class OldListsRouteADMFeatureE2ESpec - extends E2ESpec(OldListsRouteADMFeatureE2ESpec.config) +class CreateListItemsRouteADME2ESpec + extends E2ESpec(CreateListItemsRouteADME2ESpec.config) with SessionJsonProtocol with TriplestoreJsonProtocol with ListADMJsonProtocol { @@ -61,16 +59,6 @@ class OldListsRouteADMFeatureE2ESpec RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything") ) - val rootCreds: CredentialsADM = CredentialsADM( - SharedTestDataADM.rootUser, - "test" - ) - - val normalUserCreds: CredentialsADM = CredentialsADM( - SharedTestDataADM.normalUser, - "test" - ) - val anythingUserCreds: CredentialsADM = CredentialsADM( SharedTestDataADM.anythingUser1, "test" @@ -81,9 +69,7 @@ class OldListsRouteADMFeatureE2ESpec "test" ) - private val treeListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo - private val treeListNodes: Seq[ListChildNodeADM] = SharedListsTestDataADM.treeListChildNodes - private val customChildNodeIRI = "http://rdfh.ch/lists/0001/vQgijJZKSqawFooJPyhYkw" + private val customChildNodeIRI = "http://rdfh.ch/lists/0001/vQgijJZKSqawFooJPyhYkw" def addChildListNodeRequest(parentNodeIri: IRI, name: String, label: String, comment: String): String = s"""{ | "parentNodeIri": "$parentNodeIri", @@ -93,225 +79,8 @@ class OldListsRouteADMFeatureE2ESpec | "comments": [{ "value": "$comment", "language": "en"}] |}""".stripMargin - "The Lists Route (/admin/lists)" when { - "used to query information about lists" should { - "return all lists" in { - val request = - Get(baseApiUrl + s"/admin/lists") ~> addCredentials(BasicHttpCredentials(rootCreds.email, rootCreds.password)) - val response: HttpResponse = singleAwaitingRequest(request) - - response.status should be(StatusCodes.OK) - - val lists: Seq[ListNodeInfoADM] = - AkkaHttpUtils.httpResponseToJson(response).fields("lists").convertTo[Seq[ListNodeInfoADM]] - - // log.debug("lists: {}", lists) - - lists.size should be(9) - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-lists-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return all lists belonging to the images project" in { - val request = Get( - baseApiUrl + s"/admin/lists?projectIri=http%3A%2F%2Frdfh.ch%2Fprojects%2F00FF" - ) ~> addCredentials(rootCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - - response.status should be(StatusCodes.OK) - - val lists: Seq[ListNodeInfoADM] = - AkkaHttpUtils.httpResponseToJson(response).fields("lists").convertTo[Seq[ListNodeInfoADM]] - - // log.debug("lists: {}", lists) - - lists.size should be(4) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-image-project-lists-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return all lists belonging to the anything project" in { - val request = Get( - baseApiUrl + s"/admin/lists?projectIri=http%3A%2F%2Frdfh.ch%2Fprojects%2F0001" - ) ~> addCredentials(rootCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - - response.status should be(StatusCodes.OK) - - val lists: Seq[ListNodeInfoADM] = - AkkaHttpUtils.httpResponseToJson(response).fields("lists").convertTo[Seq[ListNodeInfoADM]] - - // log.debug("lists: {}", lists) - - lists.size should be(4) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-anything-project-lists-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return basic list information (w/o children)" in { - val request = Get( - baseApiUrl + s"/admin/lists/infos/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList" - ) ~> addCredentials(rootCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - - response.status should be(StatusCodes.OK) - - val receivedListInfo: ListRootNodeInfoADM = - AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] - - val expectedListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo - - receivedListInfo.sorted should be(expectedListInfo.sorted) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-list-info-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return basic list information (w/o children) for new merged GET route" in { - // the same test as above, testing the new route - val request = Get( - baseApiUrl + s"/admin/lists/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList/info" - ) ~> addCredentials(rootCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - - response.status should be(StatusCodes.OK) - - val receivedListInfo: ListRootNodeInfoADM = - AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] - - val expectedListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo - - receivedListInfo.sorted should be(expectedListInfo.sorted) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-list-info-response-new-merged-get-route", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return a complete list" in { - val request = Get( - baseApiUrl + s"/admin/lists/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList" - ) ~> addCredentials(rootCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // println(s"response: ${response.toString}") - - response.status should be(StatusCodes.OK) - - val receivedList: ListADM = AkkaHttpUtils.httpResponseToJson(response).fields("list").convertTo[ListADM] - receivedList.listinfo.sorted should be(treeListInfo.sorted) - receivedList.children.map(_.sorted) should be(treeListNodes.map(_.sorted)) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-list-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return node info w/o children" in { - val request = Get( - baseApiUrl + s"/admin/lists/nodes/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList01" - ) ~> addCredentials(rootCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - - response.status should be(StatusCodes.OK) - - val receivedListInfo: ListChildNodeInfoADM = - AkkaHttpUtils.httpResponseToJson(response).fields("nodeinfo").convertTo[ListChildNodeInfoADM] - - val expectedListInfo: ListChildNodeInfoADM = SharedListsTestDataADM.treeListNode01Info - - receivedListInfo.sorted should be(expectedListInfo.sorted) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-list-node-info-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return a complete node with children" in { - val request = Get( - baseApiUrl + s"/admin/lists/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList03" - ) ~> addCredentials(rootCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - response.status should be(StatusCodes.OK) - - val receivedNode: NodeADM = AkkaHttpUtils.httpResponseToJson(response).fields("node").convertTo[NodeADM] - receivedNode.nodeinfo.id should be("http://rdfh.ch/lists/0001/treeList03") - receivedNode.nodeinfo.name should be(Some("Tree list node 03")) - receivedNode.children.size should be(2) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "get-node-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - } - - "given a custom Iri" should { + "The admin lists route (/admin/lists)" when { + "creating list items with a custom Iri" should { "create a list with the provided custom Iri" in { val createListWithCustomIriRequest: String = s"""{ @@ -413,7 +182,6 @@ class OldListsRouteADMFeatureE2ESpec HttpEntity(ContentTypes.`application/json`, createChildNodeWithCustomIriRequest) ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) val response: HttpResponse = singleAwaitingRequest(request) - // println(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val received: ListNodeInfoADM = @@ -439,7 +207,7 @@ class OldListsRouteADMFeatureE2ESpec } } - "used to modify list information" should { + "used to create list items" should { val newListIri = new MutableTestIri val firstChildIri = new MutableTestIri val secondChildIri = new MutableTestIri @@ -532,7 +300,6 @@ class OldListsRouteADMFeatureE2ESpec val request01 = Post(baseApiUrl + s"/admin/lists", HttpEntity(ContentTypes.`application/json`, params01)) val response01: HttpResponse = singleAwaitingRequest(request01) - // println(s"response: ${response01.toString}") response01.status should be(StatusCodes.BadRequest) // invalid project IRI @@ -547,7 +314,6 @@ class OldListsRouteADMFeatureE2ESpec val request02 = Post(baseApiUrl + s"/admin/lists", HttpEntity(ContentTypes.`application/json`, params02)) val response02: HttpResponse = singleAwaitingRequest(request02) - // println(s"response: ${response02.toString}") response02.status should be(StatusCodes.BadRequest) // missing label @@ -562,250 +328,6 @@ class OldListsRouteADMFeatureE2ESpec val request03 = Post(baseApiUrl + s"/admin/lists", HttpEntity(ContentTypes.`application/json`, params03)) val response03: HttpResponse = singleAwaitingRequest(request03) - // println(s"response: ${response03.toString}") - response03.status should be(StatusCodes.BadRequest) - - } - - "update basic list information" in { - val updateListInfo: String = - s"""{ - | "listIri": "${newListIri.get}", - | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", - | "labels": [{ "value": "Neue geänderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], - | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] - |}""".stripMargin - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-list-info-request", - fileExtension = "json" - ), - text = updateListInfo - ) - ) - val encodedListUrl = java.net.URLEncoder.encode(newListIri.get, "utf-8") - - val request = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, updateListInfo) - ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - response.status should be(StatusCodes.OK) - - val receivedListInfo: ListRootNodeInfoADM = - AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] - - receivedListInfo.projectIri should be(SharedTestDataADM.ANYTHING_PROJECT_IRI) - - val labels: Seq[StringLiteralV2] = receivedListInfo.labels.stringLiterals - labels.size should be(2) - - val comments = receivedListInfo.comments.stringLiterals - comments.size should be(2) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-list-info-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "update basic list information with a new name" in { - val updateListName = - s"""{ - | "listIri": "${newListIri.get}", - | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", - | "name": "a totally new name" - |}""".stripMargin - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-list-name-request", - fileExtension = "json" - ), - text = updateListName - ) - ) - val encodedListUrl = java.net.URLEncoder.encode(newListIri.get, "utf-8") - - val request = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, updateListName) - ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - response.status should be(StatusCodes.OK) - - val receivedListInfo: ListRootNodeInfoADM = - AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] - - receivedListInfo.projectIri should be(SharedTestDataADM.ANYTHING_PROJECT_IRI) - - receivedListInfo.name should be(Some("a totally new name")) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-list-name-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "update basic list information with repeated comment and label in different languages" in { - val updateListInfoWithRepeatedCommentAndLabelValuesRequest: String = - s"""{ - | "listIri": "http://rdfh.ch/lists/0001/treeList", - | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", - | "labels": [ - | {"language": "en", "value": "Test List"}, - | {"language": "se", "value": "Test List"} - | ], - | "comments": [ - | {"language": "en", "value": "test"}, - | {"language": "de", "value": "test"}, - | {"language": "fr", "value": "test"}, - | {"language": "it", "value": "test"} - | ] - |}""".stripMargin - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-list-info-comment-label-multiple-languages-request", - fileExtension = "json" - ), - text = updateListInfoWithRepeatedCommentAndLabelValuesRequest - ) - ) - - val encodedListUrl = java.net.URLEncoder.encode("http://rdfh.ch/lists/0001/treeList", "utf-8") - - val request = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, updateListInfoWithRepeatedCommentAndLabelValuesRequest) - ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - response.status should be(StatusCodes.OK) - - val receivedListInfo: ListRootNodeInfoADM = - AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] - - receivedListInfo.projectIri should be(SharedTestDataADM.ANYTHING_PROJECT_IRI) - - val labels: Seq[StringLiteralV2] = receivedListInfo.labels.stringLiterals - labels.size should be(2) - - val comments = receivedListInfo.comments.stringLiterals - comments.size should be(4) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-list-info-comment-label-multiple-languages-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "return a ForbiddenException if the user updating the list is not project or system admin" in { - val params = - s""" - |{ - | "listIri": "${newListIri.get}", - | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", - | "labels": [{ "value": "Neue geönderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], - | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] - |} - """.stripMargin - - val encodedListUrl = java.net.URLEncoder.encode(newListIri.get, "utf-8") - - val request = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, params) - ) ~> addCredentials(anythingUserCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") - response.status should be(StatusCodes.Forbidden) - } - - "return a BadRequestException during list change when payload is not correct" in { - val encodedListUrl = java.net.URLEncoder.encode(newListIri.get, "utf-8") - - // empty list IRI - val params01 = - s""" - |{ - | "listIri": "", - | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", - | "labels": [{ "value": "Neue geönderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], - | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] - |} - """.stripMargin - - val request01 = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, params01) - ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) - val response01: HttpResponse = singleAwaitingRequest(request01) - // log.debug(s"response: ${response.toString}") - response01.status should be(StatusCodes.BadRequest) - - // empty project - val params02 = - s""" - |{ - | "listIri": "${newListIri.get}", - | "projectIri": "", - | "labels": [{ "value": "Neue geönderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], - | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] - |} - """.stripMargin - - val request02 = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, params02) - ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) - val response02: HttpResponse = singleAwaitingRequest(request02) - // log.debug(s"response: ${response.toString}") - response02.status should be(StatusCodes.BadRequest) - - // empty parameters - val params03 = - s""" - |{ - | "listIri": "${newListIri.get}", - | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", - | "labels": [], - | "comments": [{ "value": "XXXXX", "language": "en"}] - |} - """.stripMargin - - val request03 = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, params03) - ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) - val response03: HttpResponse = singleAwaitingRequest(request03) - // log.debug(s"response: ${response.toString}") response03.status should be(StatusCodes.BadRequest) } @@ -840,7 +362,6 @@ class OldListsRouteADMFeatureE2ESpec HttpEntity(ContentTypes.`application/json`, addChildToRoot) ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) val response: HttpResponse = singleAwaitingRequest(request) - // println(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val received: ListNodeInfoADM = @@ -910,7 +431,6 @@ class OldListsRouteADMFeatureE2ESpec HttpEntity(ContentTypes.`application/json`, addSecondChildToRoot) ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) val response: HttpResponse = singleAwaitingRequest(request) - // println(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val received: ListNodeInfoADM = @@ -986,7 +506,6 @@ class OldListsRouteADMFeatureE2ESpec HttpEntity(ContentTypes.`application/json`, insertChild) ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) val response: HttpResponse = singleAwaitingRequest(request) - // println(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val received: ListNodeInfoADM = @@ -1058,7 +577,6 @@ class OldListsRouteADMFeatureE2ESpec HttpEntity(ContentTypes.`application/json`, addChildToSecondChild) ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) val response: HttpResponse = singleAwaitingRequest(request) - // println(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val received: ListNodeInfoADM = @@ -1101,56 +619,6 @@ class OldListsRouteADMFeatureE2ESpec ) ) } - - "update node information of a node that has custom IRI with a new name" in { - val newName = "modified third child" - val updateNodeName = - s"""{ - | "listIri": "$customChildNodeIRI", - | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", - | "name": "${newName}" - |}""".stripMargin - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-node-info-name-request", - fileExtension = "json" - ), - text = updateNodeName - ) - ) - - val encodedListUrl = java.net.URLEncoder.encode(customChildNodeIRI, "utf-8") - - val request = Put( - baseApiUrl + s"/admin/lists/" + encodedListUrl, - HttpEntity(ContentTypes.`application/json`, updateNodeName) - ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) - val response: HttpResponse = singleAwaitingRequest(request) - - response.status should be(StatusCodes.OK) - - val receivedNodeInfo: ListChildNodeInfoADM = - AkkaHttpUtils.httpResponseToJson(response).fields("nodeinfo").convertTo[ListChildNodeInfoADM] - receivedNodeInfo.name.get should be(newName) - - clientTestDataCollector.addFile( - TestDataFileContent( - filePath = TestDataFilePath( - directoryPath = clientTestDataPath, - filename = "update-node-info-name-response", - fileExtension = "json" - ), - text = responseToString(response) - ) - ) - } - - "add flat nodes" ignore {} - "add hierarchical nodes" ignore {} - "change node order" ignore {} } } } diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala index 0d17814a2f..071baa8298 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/DeleteListItemsRouteADME2ESpec.scala @@ -75,11 +75,6 @@ class DeleteListItemsRouteADME2ESpec "test" ) - val normalUserCreds: CredentialsADM = CredentialsADM( - SharedTestDataADM.normalUser, - "test" - ) - val anythingUserCreds: CredentialsADM = CredentialsADM( SharedTestDataADM.anythingUser1, "test" @@ -90,7 +85,7 @@ class DeleteListItemsRouteADME2ESpec "test" ) - "The List Items Route (/admin/lists)" when { + "The admin lists route (/admin/lists)" when { "deleting list items" should { "return forbidden exception when requesting user is not system or project admin" in { val encodedNodeUrl = java.net.URLEncoder.encode(SharedListsTestDataADM.otherTreeListInfo.id, "utf-8") @@ -151,23 +146,24 @@ class DeleteListItemsRouteADME2ESpec val children = node.getChildren children.size should be(0) } - } - "delete a list entirely with all its children" in { - val encodedNodeUrl = java.net.URLEncoder.encode("http://rdfh.ch/lists/0001/notUsedList", "utf-8") - val request = Delete(baseApiUrl + s"/admin/lists/" + encodedNodeUrl) ~> addCredentials( - BasicHttpCredentials(anythingAdminUserCreds.user.email, anythingAdminUserCreds.password) - ) - val response: HttpResponse = singleAwaitingRequest(request) - response.status should be(StatusCodes.OK) - val deletedStatus = AkkaHttpUtils.httpResponseToJson(response).fields("deleted") - deletedStatus.convertTo[Boolean] should be(true) + "delete a list entirely with all its children" in { + val encodedNodeUrl = java.net.URLEncoder.encode("http://rdfh.ch/lists/0001/notUsedList", "utf-8") + val request = Delete(baseApiUrl + s"/admin/lists/" + encodedNodeUrl) ~> addCredentials( + BasicHttpCredentials(anythingAdminUserCreds.user.email, anythingAdminUserCreds.password) + ) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.OK) + val deletedStatus = AkkaHttpUtils.httpResponseToJson(response).fields("deleted") + deletedStatus.convertTo[Boolean] should be(true) - collectClientTestData("delete-list-response", responseToString(response)) + collectClientTestData("delete-list-response", responseToString(response)) + } } + } - "Candeletelist route (/admin/lists/candelete)" when { + "The admin lists candelete route (/admin/lists/candelete)" when { "used to query if list can be deleted" should { "return positive response for unused list" in { val unusedList = "http://rdfh.ch/lists/0001/notUsedList" @@ -216,7 +212,7 @@ class DeleteListItemsRouteADME2ESpec } } - "DeleteListNodeComments route (/admin/lists/comments)" when { + "The admin lists comments route (/admin/lists/comments)" when { "deleting comments" should { "delete child node comments" in { val childNodeIri = "http://rdfh.ch/lists/0001/testList01" diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala new file mode 100644 index 0000000000..1bb2274bea --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/GetListItemsRouteADME2ESpec.scala @@ -0,0 +1,271 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.e2e.admin.lists + +import akka.actor.ActorSystem +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.testkit.RouteTestTimeout +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory + +import scala.concurrent.duration._ + +import org.knora.webapi.E2ESpec +import org.knora.webapi.e2e.ClientTestDataCollector +import org.knora.webapi.e2e.TestDataFileContent +import org.knora.webapi.e2e.TestDataFilePath +import org.knora.webapi.messages.admin.responder.listsmessages._ +import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject +import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol +import org.knora.webapi.messages.v1.responder.sessionmessages.SessionJsonProtocol +import org.knora.webapi.messages.v1.routing.authenticationmessages.CredentialsADM +import org.knora.webapi.sharedtestdata.SharedListsTestDataADM +import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.util.AkkaHttpUtils + +object GetListItemsRouteADME2ESpec { + val config: Config = ConfigFactory.parseString(""" + akka.loglevel = "DEBUG" + akka.stdout-loglevel = "DEBUG" + """.stripMargin) +} + +/** + * End-to-End (E2E) test specification for testing lists endpoint. + */ +class GetListItemsRouteADME2ESpec + extends E2ESpec(GetListItemsRouteADME2ESpec.config) + with SessionJsonProtocol + with TriplestoreJsonProtocol + with ListADMJsonProtocol { + + implicit def default(implicit system: ActorSystem): RouteTestTimeout = RouteTestTimeout(5.seconds) + + // Directory path for generated client test data + private val clientTestDataPath: Seq[String] = Seq("admin", "lists") + + // Collects client test data + private val clientTestDataCollector = new ClientTestDataCollector(settings) + + override lazy val rdfDataObjects = List( + RdfDataObject(path = "test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"), + RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything") + ) + + val rootCreds: CredentialsADM = CredentialsADM( + SharedTestDataADM.rootUser, + "test" + ) + + private val treeListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo + private val treeListNodes: Seq[ListChildNodeADM] = SharedListsTestDataADM.treeListChildNodes + + "The admin lists route (/admin/lists)" should { + "return all lists" in { + val request = + Get(baseApiUrl + s"/admin/lists") ~> addCredentials(BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val lists: Seq[ListNodeInfoADM] = + AkkaHttpUtils.httpResponseToJson(response).fields("lists").convertTo[Seq[ListNodeInfoADM]] + + lists.size should be(9) + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-lists-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return all lists belonging to the images project" in { + val request = Get( + baseApiUrl + s"/admin/lists?projectIri=http%3A%2F%2Frdfh.ch%2Fprojects%2F00FF" + ) ~> addCredentials(rootCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val lists: Seq[ListNodeInfoADM] = + AkkaHttpUtils.httpResponseToJson(response).fields("lists").convertTo[Seq[ListNodeInfoADM]] + + lists.size should be(4) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-image-project-lists-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return all lists belonging to the anything project" in { + val request = Get( + baseApiUrl + s"/admin/lists?projectIri=http%3A%2F%2Frdfh.ch%2Fprojects%2F0001" + ) ~> addCredentials(rootCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val lists: Seq[ListNodeInfoADM] = + AkkaHttpUtils.httpResponseToJson(response).fields("lists").convertTo[Seq[ListNodeInfoADM]] + + lists.size should be(4) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-anything-project-lists-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return basic list information (w/o children)" in { + val request = Get( + baseApiUrl + s"/admin/lists/infos/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList" + ) ~> addCredentials(rootCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val receivedListInfo: ListRootNodeInfoADM = + AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] + + val expectedListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo + + receivedListInfo.sorted should be(expectedListInfo.sorted) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-list-info-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return basic list information (w/o children) for new merged GET route" in { + // the same test as above, testing the new route + val request = Get( + baseApiUrl + s"/admin/lists/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList/info" + ) ~> addCredentials(rootCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val receivedListInfo: ListRootNodeInfoADM = + AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] + + val expectedListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo + + receivedListInfo.sorted should be(expectedListInfo.sorted) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-list-info-response-new-merged-get-route", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return a complete list" in { + val request = Get( + baseApiUrl + s"/admin/lists/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList" + ) ~> addCredentials(rootCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val receivedList: ListADM = AkkaHttpUtils.httpResponseToJson(response).fields("list").convertTo[ListADM] + receivedList.listinfo.sorted should be(treeListInfo.sorted) + receivedList.children.map(_.sorted) should be(treeListNodes.map(_.sorted)) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-list-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return node info w/o children" in { + val request = Get( + baseApiUrl + s"/admin/lists/nodes/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList01" + ) ~> addCredentials(rootCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val receivedListInfo: ListChildNodeInfoADM = + AkkaHttpUtils.httpResponseToJson(response).fields("nodeinfo").convertTo[ListChildNodeInfoADM] + + val expectedListInfo: ListChildNodeInfoADM = SharedListsTestDataADM.treeListNode01Info + + receivedListInfo.sorted should be(expectedListInfo.sorted) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-list-node-info-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return a complete node with children" in { + val request = Get( + baseApiUrl + s"/admin/lists/http%3A%2F%2Frdfh.ch%2Flists%2F0001%2FtreeList03" + ) ~> addCredentials(rootCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.OK) + + val receivedNode: NodeADM = AkkaHttpUtils.httpResponseToJson(response).fields("node").convertTo[NodeADM] + receivedNode.nodeinfo.id should be("http://rdfh.ch/lists/0001/treeList03") + receivedNode.nodeinfo.name should be(Some("Tree list node 03")) + receivedNode.children.size should be(2) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "get-node-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + } +} diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala index b531ee55ae..cf9bae054e 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/UpdateListItemsRouteADME2ESpec.scala @@ -79,12 +79,13 @@ class UpdateListItemsRouteADME2ESpec "test" ) - private val treeListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo - private val treeListNodes: Seq[ListChildNodeADM] = SharedListsTestDataADM.treeListChildNodes - private val treeChildNode = treeListNodes.head + val treeListInfo: ListRootNodeInfoADM = SharedListsTestDataADM.treeListInfo + val treeListNodes: Seq[ListChildNodeADM] = SharedListsTestDataADM.treeListChildNodes + val treeChildNode = treeListNodes.head + val newListIri: String = treeListInfo.id - "The List Items Route (/admin/lists)" when { - "update list root" should { + "The admin lists route (/admin/lists)" when { + "updating list root node" should { "update only node name" in { val updateNodeName = s"""{ @@ -230,7 +231,6 @@ class UpdateListItemsRouteADME2ESpec HttpEntity(ContentTypes.`application/json`, deleteComments) ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) val response: HttpResponse = singleAwaitingRequest(request) -// log.debug(s"response: ${response.toString}") response.status should be(StatusCodes.BadRequest) } } @@ -603,5 +603,244 @@ class UpdateListItemsRouteADME2ESpec ) } } + + "updating basic list information" should { + "update basic list information" in { + val updateListInfo: String = + s"""{ + | "listIri": "${newListIri}", + | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", + | "labels": [{ "value": "Neue geänderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], + | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-list-info-request", + fileExtension = "json" + ), + text = updateListInfo + ) + ) + val encodedListUrl = java.net.URLEncoder.encode(newListIri, "utf-8") + + val request = Put( + baseApiUrl + s"/admin/lists/" + encodedListUrl, + HttpEntity(ContentTypes.`application/json`, updateListInfo) + ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.OK) + + val receivedListInfo: ListRootNodeInfoADM = + AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] + + receivedListInfo.projectIri should be(SharedTestDataADM.ANYTHING_PROJECT_IRI) + + val labels: Seq[StringLiteralV2] = receivedListInfo.labels.stringLiterals + labels.size should be(2) + + val comments = receivedListInfo.comments.stringLiterals + comments.size should be(2) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-list-info-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "update basic list information with a new name" in { + val updateListName = + s"""{ + | "listIri": "${newListIri}", + | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", + | "name": "a totally new name" + |}""".stripMargin + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-list-name-request", + fileExtension = "json" + ), + text = updateListName + ) + ) + val encodedListUrl = java.net.URLEncoder.encode(newListIri, "utf-8") + + val request = Put( + baseApiUrl + s"/admin/lists/" + encodedListUrl, + HttpEntity(ContentTypes.`application/json`, updateListName) + ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.OK) + + val receivedListInfo: ListRootNodeInfoADM = + AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] + + receivedListInfo.projectIri should be(SharedTestDataADM.ANYTHING_PROJECT_IRI) + + receivedListInfo.name should be(Some("a totally new name")) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-list-name-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "update basic list information with repeated comment and label in different languages" in { + val updateListInfoWithRepeatedCommentAndLabelValuesRequest: String = + s"""{ + | "listIri": "http://rdfh.ch/lists/0001/treeList", + | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", + | "labels": [ + | {"language": "en", "value": "Test List"}, + | {"language": "se", "value": "Test List"} + | ], + | "comments": [ + | {"language": "en", "value": "test"}, + | {"language": "de", "value": "test"}, + | {"language": "fr", "value": "test"}, + | {"language": "it", "value": "test"} + | ] + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-list-info-comment-label-multiple-languages-request", + fileExtension = "json" + ), + text = updateListInfoWithRepeatedCommentAndLabelValuesRequest + ) + ) + + val encodedListUrl = java.net.URLEncoder.encode("http://rdfh.ch/lists/0001/treeList", "utf-8") + + val request = Put( + baseApiUrl + s"/admin/lists/" + encodedListUrl, + HttpEntity(ContentTypes.`application/json`, updateListInfoWithRepeatedCommentAndLabelValuesRequest) + ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.OK) + + val receivedListInfo: ListRootNodeInfoADM = + AkkaHttpUtils.httpResponseToJson(response).fields("listinfo").convertTo[ListRootNodeInfoADM] + + receivedListInfo.projectIri should be(SharedTestDataADM.ANYTHING_PROJECT_IRI) + + val labels: Seq[StringLiteralV2] = receivedListInfo.labels.stringLiterals + labels.size should be(2) + + val comments = receivedListInfo.comments.stringLiterals + comments.size should be(4) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-list-info-comment-label-multiple-languages-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return a ForbiddenException if the user updating the list is not project or system admin" in { + val params = + s""" + |{ + | "listIri": "${newListIri}", + | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", + | "labels": [{ "value": "Neue geönderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], + | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] + |} + """.stripMargin + + val encodedListUrl = java.net.URLEncoder.encode(newListIri, "utf-8") + + val request = Put( + baseApiUrl + s"/admin/lists/" + encodedListUrl, + HttpEntity(ContentTypes.`application/json`, params) + ) ~> addCredentials(anythingUserCreds.basicHttpCredentials) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.Forbidden) + } + + "return a BadRequestException during list change when payload is not correct" in { + val encodedListUrl = java.net.URLEncoder.encode(newListIri, "utf-8") + + // empty list IRI + val params01 = + s""" + |{ + | "listIri": "", + | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", + | "labels": [{ "value": "Neue geönderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], + | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] + |} + """.stripMargin + + val request01 = Put( + baseApiUrl + s"/admin/lists/" + encodedListUrl, + HttpEntity(ContentTypes.`application/json`, params01) + ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) + val response01: HttpResponse = singleAwaitingRequest(request01) + response01.status should be(StatusCodes.BadRequest) + + // empty project + val params02 = + s""" + |{ + | "listIri": "${newListIri}", + | "projectIri": "", + | "labels": [{ "value": "Neue geönderte Liste", "language": "de"}, { "value": "Changed list", "language": "en"}], + | "comments": [{ "value": "Neuer Kommentar", "language": "de"}, { "value": "New comment", "language": "en"}] + |} + """.stripMargin + + val request02 = Put( + baseApiUrl + s"/admin/lists/" + encodedListUrl, + HttpEntity(ContentTypes.`application/json`, params02) + ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) + val response02: HttpResponse = singleAwaitingRequest(request02) + response02.status should be(StatusCodes.BadRequest) + + // empty parameters + val params03 = + s""" + |{ + | "listIri": "${newListIri}", + | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", + | "labels": [], + | "comments": [{ "value": "XXXXX", "language": "en"}] + |} + """.stripMargin + + val request03 = Put( + baseApiUrl + s"/admin/lists/" + encodedListUrl, + HttpEntity(ContentTypes.`application/json`, params03) + ) ~> addCredentials(anythingAdminUserCreds.basicHttpCredentials) + val response03: HttpResponse = singleAwaitingRequest(request03) + response03.status should be(StatusCodes.BadRequest) + + } + + } } }