From 5354bbb90b286072a98b16a28af32fb12ab075d1 Mon Sep 17 00:00:00 2001 From: Sepideh Alassi Date: Wed, 28 Oct 2020 16:08:46 +0100 Subject: [PATCH] DSP-804: create a child node with a custom IRI (#1741) * feature (customNodeIri) create a child node with a custom IRI * refactor (customChildNodeIRI) fix typo * test (createChildNodeCustomIRI) test data * feat (customChildNodeIRI) add test data automatically to expected-client-test-data list --- docs/03-apis/api-admin/lists.md | 19 +++++-- webapi/scripts/expected-client-test-data.txt | 2 + .../listsmessages/ListsMessagesADM.scala | 15 ++++-- .../responders/admin/ListsResponderADM.scala | 10 ++-- .../webapi/e2e/admin/ListsADME2ESpec.scala | 53 +++++++++++++++++++ .../listsmessages/ListsMessagesADMSpec.scala | 22 +++++++- 6 files changed, 106 insertions(+), 15 deletions(-) diff --git a/docs/03-apis/api-admin/lists.md b/docs/03-apis/api-admin/lists.md index 7f0bbc8547..0df82b73c3 100644 --- a/docs/03-apis/api-admin/lists.md +++ b/docs/03-apis/api-admin/lists.md @@ -27,7 +27,7 @@ License along with Knora. If not, see . - `GET: /admin/lists/` : return complete list with children - `POST: /admin/lists` : create new list - `PUT: /admin/lists/` : update list information -- `POST: /admin/lists/` : create new child node under the supplied parent node IRI +- `POST: /admin/lists/` : create new child node under the supplied parent node IRI - NOT IMPLEMENTED: `DELETE: /admin/lists/` : delete list including children if not used - `GET: /admin/lists/infos/` : return list information (without children) @@ -83,12 +83,12 @@ Additionally, each list can have an optional custom IRI (of [Knora IRI](../api-v - Appends a new child node under the supplied nodeIri. If the supplied nodeIri is the listIri, then a new child node is appended to the top level. Children are currently only appended. - - POST: `/admin/lists/` + - POST: `/admin/lists/` - BODY: ```json { - "parentNodeIri": "nodeIri", + "parentNodeIri": "parentNodeIri", "projectIri": "someprojectiri", "name": "first", "labels": [{ "value": "New First Child List Node Value", "language": "en"}], @@ -96,6 +96,19 @@ Additionally, each list can have an optional custom IRI (of [Knora IRI](../api-v } ``` +Additionally, each child node can have an optional custom IRI (of [Knora IRI](../api-v2/knora-iris.md#iris-for-data) form) specified by the `id` in the request body as below: + +```json + { + "id": "http://rdfh.ch/lists/0001/a-child-node-with-IRI", + "parentNodeIri": "http://rdfh.ch/lists/0001/a-list-with-IRI", + "projectIri": "http://rdfh.ch/projects/0001", + "name": "child node with a custom IRI", + "labels": [{ "value": "New child node with IRI", "language": "en"}], + "comments": [{ "value": "New child node comment", "language": "en"}] + } +``` + ### Get list's information - Required permission: none diff --git a/webapi/scripts/expected-client-test-data.txt b/webapi/scripts/expected-client-test-data.txt index a6fd485241..2443fb0fa5 100644 --- a/webapi/scripts/expected-client-test-data.txt +++ b/webapi/scripts/expected-client-test-data.txt @@ -18,6 +18,8 @@ test-data/admin/lists/add-second-child-to-root-request.json test-data/admin/lists/add-second-child-to-root-response.json test-data/admin/lists/create-child-node-request.json test-data/admin/lists/create-child-node-response.json +test-data/admin/lists/create-child-node-with-custom-IRI-request.json +test-data/admin/lists/create-child-node-with-custom-IRI-response.json test-data/admin/lists/create-list-request.json test-data/admin/lists/create-list-response.json test-data/admin/lists/create-list-with-custom-IRI-request.json diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala index 1f159d3af9..a86870c0ef 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADM.scala @@ -79,17 +79,22 @@ case class CreateListApiRequestADM(id: Option[IRI] = None, * is added can be either a root list node or a child list node. At least one label needs to be supplied. If other * child nodes exist, the newly created list node will be appended to the end. * - * @param parentNodeIri - * @param labels - * @param comments + * @param id the optional custom IRI of the list node. + * @param parentNodeIri the IRI of the parent node. + * @param projectIri the IRI of the project. + * @param name the optional name of the list node. + * @param labels labels of the list node. + * @param comments comments of the list node. */ -case class CreateChildNodeApiRequestADM(parentNodeIri: IRI, +case class CreateChildNodeApiRequestADM(id: Option[IRI] = None, + parentNodeIri: IRI, projectIri: IRI, name: Option[String], labels: Seq[StringLiteralV2], comments: Seq[StringLiteralV2]) extends ListADMJsonProtocol { private val stringFormatter = StringFormatter.getInstanceForConstantOntologies + stringFormatter.validateOptionalListIri(id, throw BadRequestException(s"Invalid list node IRI")) if (parentNodeIri.isEmpty) { // println(this) @@ -967,7 +972,7 @@ trait ListADMJsonProtocol extends SprayJsonSupport with DefaultJsonProtocol with implicit val createListApiRequestADMFormat: RootJsonFormat[CreateListApiRequestADM] = jsonFormat(CreateListApiRequestADM, "id", "projectIri", "name", "labels", "comments") - implicit val createListNodeApiRequestADMFormat: RootJsonFormat[CreateChildNodeApiRequestADM] = jsonFormat(CreateChildNodeApiRequestADM, "parentNodeIri", "projectIri", "name", "labels", "comments") + implicit val createListNodeApiRequestADMFormat: RootJsonFormat[CreateChildNodeApiRequestADM] = jsonFormat(CreateChildNodeApiRequestADM, "id" , "parentNodeIri", "projectIri", "name", "labels", "comments") implicit val changeListInfoApiRequestADMFormat: RootJsonFormat[ChangeListInfoApiRequestADM] = jsonFormat(ChangeListInfoApiRequestADM, "listIri", "projectIri", "name", "labels", "comments") implicit val nodePathGetResponseADMFormat: RootJsonFormat[NodePathGetResponseADM] = jsonFormat(NodePathGetResponseADM, "elements") implicit val listsGetResponseADMFormat: RootJsonFormat[ListsGetResponseADM] = jsonFormat(ListsGetResponseADM, "lists") diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala index f0b0500bc0..4208be486f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala @@ -617,13 +617,12 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde throw BadRequestException(s"The node name ${createListRequest.name.get} is already used by a list inside the project ${createListRequest.projectIri}.") } - maybeShortcode = project.shortcode - dataNamedGraph = stringFormatter.projectDataNamedGraphV2(project) - // check the custom IRI; if not given, create an unused IRI customListIri: Option[SmartIri] = createListRequest.id.map(iri => iri.toSmartIri) + maybeShortcode = project.shortcode listIri: IRI <- checkOrCreateEntityIri(customListIri, stringFormatter.makeRandomListIri(maybeShortcode)) + dataNamedGraph = stringFormatter.projectDataNamedGraphV2(project) // Create the new list createNewListSparqlString = org.knora.webapi.messages.twirl.queries.sparql.admin.txt.createNewList( dataNamedGraph = dataNamedGraph, @@ -826,9 +825,10 @@ class ListsResponderADM(responderData: ResponderData) extends Responder(responde // calculate the data named graph dataNamedGraph = stringFormatter.projectDataNamedGraphV2(project) - // calculate the new node's IRI + // check the custom IRI; if not given, create an unused IRI + customListIri: Option[SmartIri] = createChildNodeRequest.id.map(iri => iri.toSmartIri) maybeShortcode = project.shortcode - newListNodeIri = stringFormatter.makeRandomListIri(maybeShortcode) + newListNodeIri: IRI <- checkOrCreateEntityIri(customListIri, stringFormatter.makeRandomListIri(maybeShortcode)) // Create the new list node createNewListSparqlString = org.knora.webapi.messages.twirl.queries.sparql.admin.txt.createNewListChildNode( diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/ListsADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/ListsADME2ESpec.scala index 294c9e3d00..7c5b4b8ca2 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/ListsADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/ListsADME2ESpec.scala @@ -319,6 +319,59 @@ class ListsADME2ESpec extends E2ESpec(ListsADME2ESpec.config) with SessionJsonPr val invalidIri: Boolean = errorMessage.contains(s"IRI: '${SharedTestDataADM.customListIRI}' already exists, try another one.") invalidIri should be(true) } + + "add a child with a custom IRI" in { + + val customChildNodeIRI = "http://rdfh.ch/lists/0001/a-child-node-with-IRI" + + val createChildNodeWithCustomIriRequest = + s""" + |{ "id": "$customChildNodeIRI", + | "parentNodeIri": "${SharedTestDataADM.customListIRI}", + | "projectIri": "${SharedTestDataADM.ANYTHING_PROJECT_IRI}", + | "name": "node with a custom IRI", + | "labels": [{ "value": "New List Node", "language": "en"}], + | "comments": [] + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "create-child-node-with-custom-IRI-request", + fileExtension = "json" + ), + text = createChildNodeWithCustomIriRequest + ) + ) + + val encodedParentNodeUrl = java.net.URLEncoder.encode(SharedTestDataADM.customListIRI, "utf-8") + + val request = Post(baseApiUrl + s"/admin/lists/" + encodedParentNodeUrl, 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 = AkkaHttpUtils.httpResponseToJson(response).fields("nodeinfo").convertTo[ListNodeInfoADM] + + // check correct node info + val childNodeInfo = received match { + case info: ListChildNodeInfoADM => info + case something => fail(s"expecting ListChildNodeInfoADM but got ${something.getClass.toString} instead.") + } + childNodeInfo.id should be (customChildNodeIRI) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "create-child-node-with-custom-IRI-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } } "used to modify list information" should { diff --git a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala index c2b6d63f79..c9aba0224e 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala @@ -262,7 +262,7 @@ class ListsMessagesADMSpec extends AnyWordSpecLike with Matchers with ListADMJso thrown.getMessage should equal (UPDATE_REQUEST_EMPTY_LABEL_OR_COMMENT_ERROR) } - "throw 'BadRequestException' for `CreateChildNodeApiRequestADM` when list node iri is empty" in { + "throw 'BadRequestException' for `CreateChildNodeApiRequestADM` when no parent node iri is given" in { val payload = s""" @@ -280,7 +280,7 @@ class ListsMessagesADMSpec extends AnyWordSpecLike with Matchers with ListADMJso } - "throw 'BadRequestException' for `CreateChildNodeApiRequestADM` when list node iri is invalid" in { + "throw 'BadRequestException' for `CreateChildNodeApiRequestADM` when parent node iri is invalid" in { val payload = s""" @@ -352,5 +352,23 @@ class ListsMessagesADMSpec extends AnyWordSpecLike with Matchers with ListADMJso } + "throw 'BadRequestException' for `CreateChildNodeApiRequestADM` when custom iri of the child node is invalid" in { + + val payload = + s""" + |{ "id": "invalid-list-node-IRI", + | "parentNodeIri": "$exampleListIri", + | "projectIri": "${SharedTestDataADM.IMAGES_PROJECT_IRI}", + | "labels": [{ "value": "Neuer List Node", "language": "de"}], + | "comments": [] + |} + """.stripMargin + + val thrown = the [BadRequestException] thrownBy payload.parseJson.convertTo[CreateChildNodeApiRequestADM] + + thrown.getMessage should equal ("Invalid list node IRI") + + } + } }